diff --git a/CMakeLists.txt b/CMakeLists.txt index afde961f4299b5d696dec21d80c8979638d8e694..808abea077f5d36e6135514fa1ad2a89b265e330 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ find_package(KF5Config ${KF5_VERSION} CONFIG REQUIRED) find_package(KF5DBusAddons ${KF5_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiNotes ${AKONADINOTES_LIB_VERSION} CONFIG REQUIRED) find_package(KF5XmlGui ${KF5_VERSION} CONFIG REQUIRED) +find_package(KF5Declarative ${KF5_VERSION} CONFIG REQUIRED) set(MAILCOMMN_LIB_VERSION "5.2.40") set(GRAVATAR_LIB_VERSION "5.2.40") @@ -69,6 +70,7 @@ set(GRAVATAR_LIB_VERSION "5.2.40") set(INCIDENCEEDITOR_LIB_VERSION "5.2.40") set(KTNEF_LIB_VERSION "5.2.40") set(MESSAGELIB_LIB_VERSION "5.2.62") +set(AKONADICALENDAR_LIB_VERSION "5.2.40") find_package(KF5WebEngineViewer ${MESSAGELIB_LIB_VERSION} CONFIG REQUIRED) find_package(KF5MailCommon ${MAILCOMMN_LIB_VERSION} CONFIG REQUIRED) @@ -85,6 +87,7 @@ find_package(KF5MessageList ${MESSAGELIB_LIB_VERSION} CONFIG REQUIRED) find_package(KF5CalendarSupport ${CALENDARSUPPORT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5EventViews ${EVENTVIEW_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Akonadi ${AKONADI_VERSION} CONFIG REQUIRED) +find_package(KF5AkonadiCalendar ${AKONADICALENDAR_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Gravatar ${GRAVATAR_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Tnef ${KTNEF_LIB_VERSION} CONFIG REQUIRED) diff --git a/kdepim-addons.categories b/kdepim-addons.categories index 362b6d0874ecca4020f20075aa4445d7b5090898..c566ee1f69326ba3eb1b3248a2fcb7fbdb37bd99 100644 --- a/kdepim-addons.categories +++ b/kdepim-addons.categories @@ -16,3 +16,5 @@ log_externalscriptplugin kdepim-addons (External Script Plugin) log_text_calendar kdepim-addons (messageviewer calendar plugins) log_ms_tnef kdepim-addons (ms_tnef) log_vcard kdepim-addons (vcard) +log_pimeventsplugin kdepim-addons (Plasma calendar plugin) + diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 24103acf805b13e65bf5057cf4d66e2686a2d53b..cc1d1261e21076469d6322e6ba83257d38ed2a13 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -3,5 +3,6 @@ add_subdirectory(messageviewerplugins) add_subdirectory(messageviewerheaderplugins) add_subdirectory(storageservices) add_subdirectory(messageviewer) +add_subdirectory(plasma) add_subdirectory(webengineurlinterceptor) diff --git a/plugins/plasma/CMakeLists.txt b/plugins/plasma/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..538c67669426e0515ee70f662bc467d9fa74811e --- /dev/null +++ b/plugins/plasma/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(pimeventsplugin) diff --git a/plugins/plasma/pimeventsplugin/CMakeLists.txt b/plugins/plasma/pimeventsplugin/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..79efc17c6cecd1b90399f9956ceb450a85dcf885 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/CMakeLists.txt @@ -0,0 +1,57 @@ +if (BUILD_TESTING) + add_subdirectory(autotests) +endif() + +set(pimeventsplugin_SRCS + pimeventsplugin.cpp + akonadipimdatasource.cpp + eventdatavisitor.cpp + settingschangenotifier.cpp +) + +ecm_qt_declare_logging_category(loggingcategory_SRCS + HEADER pimeventsplugin_debug.h + IDENTIFIER PIMEVENTSPLUGIN_LOG + CATEGORY_NAME log_pimeventsplugin +) + +add_library(pimevents MODULE ${pimeventsplugin_SRCS} ${loggingcategory_SRCS}) +target_link_libraries(pimevents + Qt5::Core + KF5::AkonadiCore + KF5::AkonadiCalendar + KF5::CalendarCore + KF5::CalendarEvents + KF5::EventViews # for reading KOrganizer calendar colors +) + +install(TARGETS pimevents + DESTINATION ${PLUGIN_INSTALL_DIR}/plasmacalendarplugins +) + + +######################### NEXT TARGET #######################33 + +set(plasmapimcalendarsplugin_SRCS + pimcalendarsplugin.cpp + pimcalendarsmodel.cpp + settingschangenotifier.cpp +) + +add_library(pimcalendarsplugin SHARED ${plasmapimcalendarsplugin_SRCS} ${loggingcategory_SRCS}) +target_link_libraries(pimcalendarsplugin + Qt5::Core + Qt5::Qml + KF5::AkonadiCore + KF5::CalendarCore +) + +install(TARGETS pimcalendarsplugin + DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/PimCalendars +) +install(FILES qmldir + DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/PimCalendars +) +install(FILES PimEventsConfig.qml + DESTINATION ${PLUGIN_INSTALL_DIR}/plasmacalendarplugins/pimevents +) diff --git a/plugins/plasma/pimeventsplugin/PimEventsConfig.qml b/plugins/plasma/pimeventsplugin/PimEventsConfig.qml new file mode 100644 index 0000000000000000000000000000000000000000..3e1a8444f4213cdcd1c32f81a9445e9d13ea7773 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/PimEventsConfig.qml @@ -0,0 +1,112 @@ +/* + * Copyright 2016 Daniel Vrátil + * + * 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, see + */ + +import QtQuick 2.0 +import QtQuick.Controls 1.4 +// gives us TextSingleton +import QtQuick.Controls.Private 1.0 +import QtQuick.Layouts 1.0 +import QtQuick.Dialogs 1.1 +import org.kde.plasma.core 2.0 +import org.kde.plasma.PimCalendars 1.0 + +Item { + id: pimEventsConfig + width: parent.width + height: parent.height + + signal configurationChanged + + function saveConfig() + { + calendarModel.saveConfig(); + } + + PimCalendarsModel { + id: calendarModel + } + + // Invisible, used to measure implicitHeight of checkboxes so we can + // adjust row height in rowDelegate + CheckBox { + id: checkboxSize + visible: false + } + + TreeView { + id: calendarTreeView; + + anchors.fill: parent + + model: calendarModel + + TableViewColumn { + role: "data" + title: "Select Calendars"; + delegate: Item { + CheckBox { + id: checkbox + visible: styleData.value["enabled"] + checked: styleData.value["checked"] + onCheckedChanged: { + if (checked == styleData.value["checked"]) { + return; + } + calendarModel.setChecked(styleData.value["id"], checked); + pimEventsConfig.configurationChanged(); + } + width: 24 + height: 24 + } + IconItem { + id: icon + anchors.left: checkbox.visible ? checkbox.right : parent.left + visible: valid + source: styleData.value["iconName"] + height: 20 + width: 20 + } + Text { + anchors.left: icon.visible ? icon.right : checkbox.visible ? checkbox.right : parent.left + + text: styleData.value["name"] + horizontalAlignment: Qt.AlignLeft + verticalAlignment: Qt.AlignVCenter + color: styleData.textColor + height: 24 + } + } + } + + // Based on Desktop.TableViewStyle + rowDelegate: BorderImage { + visible: styleData.selected || styleData.alternate + source: "image://__tablerow/" + (styleData.alternate ? "alternate_" : "") + + (styleData.selected ? "selected_" : "") + + (calendarTreeView.activeFocus ? "active" : "") + // Make sure the checkbox always fits, add 4 for some small margin + height: Math.min(checkboxSize.implicitHeight, Math.max(16, TextSingleton.implicitHeight * 1.2)) + 4 + border { + left: 4 + right: 4 + } + } + } +} diff --git a/plugins/plasma/pimeventsplugin/akonadipimdatasource.cpp b/plugins/plasma/pimeventsplugin/akonadipimdatasource.cpp new file mode 100644 index 0000000000000000000000000000000000000000..397046974e99be56374bb971d0b153717199c624 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/akonadipimdatasource.cpp @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 "akonadipimdatasource.h" +#include "settingschangenotifier.h" +#include "pimeventsplugin_debug.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +AkonadiPimDataSource::AkonadiPimDataSource(QObject *parent) + : QObject(parent) +{ + Akonadi::AttributeFactory::registerAttribute(); + + connect(SettingsChangeNotifier::self(), &SettingsChangeNotifier::settingsChanged, + this, &AkonadiPimDataSource::onSettingsChanged); + + mMonitor = new Akonadi::ChangeRecorder(this); + mMonitor->setChangeRecordingEnabled(false); + mMonitor->itemFetchScope().fetchFullPayload(true); + mMonitor->itemFetchScope().fetchAttribute(); + mMonitor->itemFetchScope().fetchAttribute(); + onSettingsChanged(); + + mCalendar = new Akonadi::ETMCalendar(mMonitor, this); + // TOOD: Only retrieve PLD:HEAD once it's supported + mCalendar->setCollectionFilteringEnabled(false); + + // Would be nice to have a proper API to read KOrganizer calendar colors... + KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("korganizerrc")); + KCoreConfigSkeleton *skel = new KCoreConfigSkeleton(config); + mEventViewsPrefs = EventViews::PrefsPtr(new EventViews::Prefs(skel)); + mEventViewsPrefs->readConfig(); +} + +AkonadiPimDataSource::~AkonadiPimDataSource() +{ +} + +qint64 AkonadiPimDataSource::akonadiIdForIncidence(const KCalCore::Incidence::Ptr &incidence) const +{ + return mCalendar->item(incidence).id(); +} + +KCalCore::Calendar *AkonadiPimDataSource::calendar() const +{ + return mCalendar; +} + +QString AkonadiPimDataSource::calendarColorForIncidence(const KCalCore::Incidence::Ptr &incidence) const +{ + const auto &item = mCalendar->item(incidence); + if (!item.isValid()) { + return QString(); + } + + const auto &col = mCalendar->collection(item.parentCollection().id()); + if (!col.isValid()) { + return QString(); + } + + auto it = mColorCache.find(col.id()); + if (it == mColorCache.end()) { + if (col.hasAttribute()) { + const auto attr = col.attribute(); + it = mColorCache.insert(col.id(), attr->color().name()); + } else { + QColor color = mEventViewsPrefs->resourceColorKnown(QString::number(col.id())); + if (color.isValid()) { + it = mColorCache.insert(col.id(), color.name()); + } else { + it = mColorCache.insert(col.id(), QString()); + } + } + } + return (*it); +} + +void AkonadiPimDataSource::onSettingsChanged() +{ + QSet currentCols; + Q_FOREACH (const Akonadi::Collection &col, mMonitor->collectionsMonitored()) { + currentCols.insert(col); + } + + auto config = KSharedConfig::openConfig(); + auto group = config->group("PIMEventsPlugin"); + const QList calendars = group.readEntry(QStringLiteral("calendars"), QList()); + QSet configuredCols; + Q_FOREACH (qint64 colId, calendars) { + configuredCols.insert(Akonadi::Collection(colId)); + } + + Q_FOREACH (const Akonadi::Collection &col, (currentCols - configuredCols)) { + mMonitor->setCollectionMonitored(col, false); + } + Q_FOREACH (const Akonadi::Collection &col, (configuredCols - currentCols)) { + mMonitor->setCollectionMonitored(col, true); + } + + const bool hasSelectedCols = mMonitor->collectionsMonitored().isEmpty(); + mMonitor->setMimeTypeMonitored(KCalCore::Event::eventMimeType(), hasSelectedCols); + mMonitor->setMimeTypeMonitored(KCalCore::Todo::todoMimeType(), hasSelectedCols); +} diff --git a/plugins/plasma/pimeventsplugin/akonadipimdatasource.h b/plugins/plasma/pimeventsplugin/akonadipimdatasource.h new file mode 100644 index 0000000000000000000000000000000000000000..e1da64e991285d6c3e0dbf53d74fe164fb73ca7c --- /dev/null +++ b/plugins/plasma/pimeventsplugin/akonadipimdatasource.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 AKONADIPIMDATASOURCE_H +#define AKONADIPIMDATASOURCE_H + +#include "pimdatasource.h" +#include +#include + +namespace Akonadi { +class ChangeRecorder; +class ETMCalendar; +} + +class AkonadiPimDataSource : public QObject, + public PimDataSource +{ + Q_OBJECT + +public: + explicit AkonadiPimDataSource(QObject *parent = Q_NULLPTR); + ~AkonadiPimDataSource(); + + qint64 akonadiIdForIncidence(const KCalCore::Incidence::Ptr &incidence) const Q_DECL_OVERRIDE; + KCalCore::Calendar *calendar() const Q_DECL_OVERRIDE; + QString calendarColorForIncidence(const KCalCore::Incidence::Ptr &incidence) const Q_DECL_OVERRIDE; + +private Q_SLOTS: + void onSettingsChanged(); + +private: + Akonadi::ChangeRecorder *mMonitor; + Akonadi::ETMCalendar *mCalendar; + EventViews::PrefsPtr mEventViewsPrefs; + mutable QHash mColorCache; +}; + + +#endif diff --git a/plugins/plasma/pimeventsplugin/autotests/CMakeLists.txt b/plugins/plasma/pimeventsplugin/autotests/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..44a6d775b360b7588f4237ba7b1ccbda88d5ec08 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/CMakeLists.txt @@ -0,0 +1,27 @@ +macro(add_plasma_pimeventsplugin_test _source _additional) + set(_test ${_source} + ${_additional} + testdataparser.cpp + fakepimdatasource.cpp + ${CMAKE_CURRENT_BINARY_DIR}/../pimeventsplugin_debug.cpp + ) + get_filename_component(_name ${_source} NAME_WE) + add_executable(${_name} ${_test}) + add_test(${_name} ${_name}) + ecm_mark_as_test(plasma-pimeventsplugin-${_name}) + add_definitions(-DQT_TESTCASE_BUILDDIR=\"${CMAKE_CURRENT_SOURCE_DIR}\") + target_link_libraries(${_name} Qt5::Core + Qt5::Test + KF5::AkonadiCore + KF5::AkonadiCalendar + KF5::CalendarCore + KF5::CalendarEvents + KF5::EventViews + ) + include_directories(${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_CURRENT_BINARY_DIR}/.. + ) +endmacro() + +add_plasma_pimeventsplugin_test(eventdatavisitortest.cpp ../eventdatavisitor.cpp) +add_plasma_pimeventsplugin_test(pimeventsplugintest.cpp "../eventdatavisitor.cpp;../pimeventsplugin.cpp;../akonadipimdatasource.cpp;../settingschangenotifier.cpp") diff --git a/plugins/plasma/pimeventsplugin/autotests/data/allday-multiday-event.ics b/plugins/plasma/pimeventsplugin/autotests/data/allday-multiday-event.ics new file mode 100644 index 0000000000000000000000000000000000000000..101bc04ec762ca14825de9d492a52f1a6813f8a0 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/allday-multiday-event.ics @@ -0,0 +1,13 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTAMP:20160529T143409Z +CREATED:20160529T143351Z +UID:43f8fa38-6794-4862-a389-11e78d1ad584 +LAST-MODIFIED:20160529T143409Z +DESCRIPTION:This is an all-day multi-day event +SUMMARY:Multi day all day event +DTSTART;VALUE=DATE:20160528 +DTEND;VALUE=DATE:20160531 +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/plugins/plasma/pimeventsplugin/autotests/data/allday-multiday-event.json b/plugins/plasma/pimeventsplugin/autotests/data/allday-multiday-event.json new file mode 100644 index 0000000000000000000000000000000000000000..71bbe9f8cdd09478339b8a3c00eb70fd23b8a886 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/allday-multiday-event.json @@ -0,0 +1,23 @@ +{ + "rangeStart": "2016-05-01", + "rangeEnd": "2016-05-31", + "akonadiId": 1, + "eventData": [ + { + "summary": "Multi day all day event", + "description": "This is an all-day multi-day event", + "type": "Event", + "allDay": true, + "isMinor": false, + "startDateTime": { + "date": "2016-05-28", + "time": "" + }, + "endDateTime": { + "date": "2016-05-30", + "time": "" + }, + "uid": "Akonadi-1" + } + ] +} diff --git a/plugins/plasma/pimeventsplugin/autotests/data/allday-oneday-event.ics b/plugins/plasma/pimeventsplugin/autotests/data/allday-oneday-event.ics new file mode 100644 index 0000000000000000000000000000000000000000..91dea0d9292044d30f03e07050eb8f127fcdab8a --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/allday-oneday-event.ics @@ -0,0 +1,13 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTAMP:20160529T143104Z +CREATED:20160529T143019Z +UID:c843300a-856d-4567-8ecc-a29255b6f5f5 +LAST-MODIFIED:20160529T143104Z +DESCRIPTION:This is an all-day one day non-recurring event. +SUMMARY:All day one day non-recurring event +DTSTART;VALUE=DATE:20160529 +DTEND;VALUE=DATE:20160530 +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/plugins/plasma/pimeventsplugin/autotests/data/allday-oneday-event.json b/plugins/plasma/pimeventsplugin/autotests/data/allday-oneday-event.json new file mode 100644 index 0000000000000000000000000000000000000000..5c279b950a5a4e51b4e55ae5744de8a08076bf4d --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/allday-oneday-event.json @@ -0,0 +1,23 @@ +{ + "rangeStart": "2016-05-01", + "rangeEnd": "2016-05-31", + "akonadiId": 2, + "eventData": [ + { + "summary": "All day one day non-recurring event", + "description": "This is an all-day one day non-recurring event.", + "type": "Event", + "allDay": true, + "isMinor": false, + "startDateTime": { + "date": "2016-05-29", + "time": "" + }, + "endDateTime": { + "date": "2016-05-29", + "time": "" + }, + "uid": "Akonadi-2" + } + ] +} diff --git a/plugins/plasma/pimeventsplugin/autotests/data/allday-recurring-multiday-event.ics b/plugins/plasma/pimeventsplugin/autotests/data/allday-recurring-multiday-event.ics new file mode 100644 index 0000000000000000000000000000000000000000..1c8bbc241c83e519e338aff39d0ba3c272dc6c91 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/allday-recurring-multiday-event.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTAMP:20160529T143448Z +CREATED:20160529T143412Z +UID:1fceddbc-d1b9-4e20-8fea-1d36e191a42d +LAST-MODIFIED:20160529T143448Z +DESCRIPTION:This is an all-day multi-day recurring event +SUMMARY:All day multi-day recurring event +RRULE:FREQ=WEEKLY;BYDAY=FR +DTSTART;VALUE=DATE:20160408 +DTEND;VALUE=DATE:20160411 +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/plugins/plasma/pimeventsplugin/autotests/data/allday-recurring-multiday-event.json b/plugins/plasma/pimeventsplugin/autotests/data/allday-recurring-multiday-event.json new file mode 100644 index 0000000000000000000000000000000000000000..590a5f199e1ed02b14b7b37958a064a0c48d5422 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/allday-recurring-multiday-event.json @@ -0,0 +1,74 @@ +{ + "rangeStart": "2016-05-01", + "rangeEnd": "2016-05-31", + "akonadiId": 3, + "eventData": [ + { + "summary": "All day multi-day recurring event", + "description": "This is an all-day multi-day recurring event", + "type": "Event", + "allDay": true, + "isMinor": false, + "startDateTime": { + "date": "2016-05-06", + "time": "" + }, + "endDateTime": { + "date": "2016-05-08", + "time": "" + }, + "uid": "Akonadi-3-20160506T000000" + }, + + { + "summary": "All day multi-day recurring event", + "description": "This is an all-day multi-day recurring event", + "type": "Event", + "allDay": true, + "isMinor": false, + "startDateTime": { + "date": "2016-05-13", + "time": "" + }, + "endDateTime": { + "date": "2016-05-15", + "time": "" + }, + "uid": "Akonadi-3-20160513T000000" + }, + + { + "summary": "All day multi-day recurring event", + "description": "This is an all-day multi-day recurring event", + "type": "Event", + "allDay": true, + "isMinor": false, + "startDateTime": { + "date": "2016-05-20", + "time": "" + }, + "endDateTime": { + "date": "2016-05-22", + "time": "" + }, + "uid": "Akonadi-3-20160520T000000" + }, + + { + "summary": "All day multi-day recurring event", + "description": "This is an all-day multi-day recurring event", + "type": "Event", + "allDay": true, + "isMinor": false, + "startDateTime": { + "date": "2016-05-27", + "time": "" + }, + "endDateTime": { + "date": "2016-05-29", + "time": "" + }, + "uid": "Akonadi-3-20160527T000000" + } + ] +} diff --git a/plugins/plasma/pimeventsplugin/autotests/data/allday-recurring-oneday-event.ics b/plugins/plasma/pimeventsplugin/autotests/data/allday-recurring-oneday-event.ics new file mode 100644 index 0000000000000000000000000000000000000000..0a4408f60645f46a72b00347bbfe72aa4096e2e6 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/allday-recurring-oneday-event.ics @@ -0,0 +1,15 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTAMP:20160529T143257Z +CREATED:20160529T143223Z +UID:9bcaf475-021e-4396-beec-e8206298cade +SEQUENCE:2 +LAST-MODIFIED:20160529T143257Z +DESCRIPTION:This is a one day all-day recurring event +SUMMARY:All day one day recurring event +RRULE:FREQ=WEEKLY;BYDAY=FR +DTSTART;VALUE=DATE:20160401 +DTEND;VALUE=DATE:20160402 +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/plugins/plasma/pimeventsplugin/autotests/data/allday-recurring-oneday-event.json b/plugins/plasma/pimeventsplugin/autotests/data/allday-recurring-oneday-event.json new file mode 100644 index 0000000000000000000000000000000000000000..28b1fc3859c0d68796f1c471e393a3dbda7da682 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/allday-recurring-oneday-event.json @@ -0,0 +1,74 @@ +{ + "rangeStart": "2016-05-01", + "rangeEnd": "2016-05-31", + "akonadiId": 4, + "eventData": [ + { + "summary": "All day one day recurring event", + "description": "This is a one day all-day recurring event", + "type": "Event", + "allDay": true, + "isMinor": false, + "startDateTime": { + "date": "2016-05-06", + "time": "" + }, + "endDateTime": { + "date": "2016-05-06", + "time": "" + }, + "uid": "Akonadi-4-20160506T000000" + }, + + { + "summary": "All day one day recurring event", + "description": "This is a one day all-day recurring event", + "type": "Event", + "allDay": true, + "isMinor": false, + "startDateTime": { + "date": "2016-05-13", + "time": "" + }, + "endDateTime": { + "date": "2016-05-13", + "time": "" + }, + "uid": "Akonadi-4-20160513T000000" + }, + + { + "summary": "All day one day recurring event", + "description": "This is a one day all-day recurring event", + "type": "Event", + "allDay": true, + "isMinor": false, + "startDateTime": { + "date": "2016-05-20", + "time": "" + }, + "endDateTime": { + "date": "2016-05-20", + "time": "" + }, + "uid": "Akonadi-4-20160520T000000" + }, + + { + "summary": "All day one day recurring event", + "description": "This is a one day all-day recurring event", + "type": "Event", + "allDay": true, + "isMinor": false, + "startDateTime": { + "date": "2016-05-27", + "time": "" + }, + "endDateTime": { + "date": "2016-05-27", + "time": "" + }, + "uid": "Akonadi-4-20160527T000000" + } + ] +} diff --git a/plugins/plasma/pimeventsplugin/autotests/data/simple-multiday-event.ics b/plugins/plasma/pimeventsplugin/autotests/data/simple-multiday-event.ics new file mode 100644 index 0000000000000000000000000000000000000000..a0893d1769a8e5d15e83b18b15b9efd8f537d4ac --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/simple-multiday-event.ics @@ -0,0 +1,13 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTAMP:20160529T143349Z +CREATED:20160529T143303Z +UID:afa77766-4690-44a3-959f-f5e6cc70043a +LAST-MODIFIED:20160529T143349Z +DESCRIPTION:This is a simple multi day event +SUMMARY:Simple multi day event +DTSTART;TZID=Europe/Prague:20160528T160000 +DTEND;TZID=Europe/Prague:20160530T120000 +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/plugins/plasma/pimeventsplugin/autotests/data/simple-multiday-event.json b/plugins/plasma/pimeventsplugin/autotests/data/simple-multiday-event.json new file mode 100644 index 0000000000000000000000000000000000000000..ce991a8ad5309c4d6e709e94ca3c67b61c4aaf87 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/simple-multiday-event.json @@ -0,0 +1,25 @@ +{ + "rangeStart": "2016-05-01", + "rangeEnd": "2016-05-31", + "akonadiId": 5, + "eventData": [ + { + "summary": "Simple multi day event", + "description": "This is a simple multi day event", + "type": "Event", + "allDay": false, + "isMinor": false, + "startDateTime": { + "date": "2016-05-28", + "time": "16:00:00", + "tz": "Europe/Prague" + }, + "endDateTime": { + "date": "2016-05-30", + "time": "12:00:00", + "tz": "Europe/Prague" + }, + "uid": "Akonadi-5" + } + ] +} diff --git a/plugins/plasma/pimeventsplugin/autotests/data/simple-oneday-event.ics b/plugins/plasma/pimeventsplugin/autotests/data/simple-oneday-event.ics new file mode 100644 index 0000000000000000000000000000000000000000..19a254517bbd5b91990cc3c8d41e3d1a938b316c --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/simple-oneday-event.ics @@ -0,0 +1,13 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTAMP:20160529T143018Z +CREATED:20160529T142843Z +UID:4d7fdd2c-2d3a-4ecf-8964-00eb92225209 +LAST-MODIFIED:20160529T143018Z +DESCRIPTION:This is a simple one-day non-recurring event. +SUMMARY:Simple one day non-recurring event +DTSTART;TZID=Europe/Prague:20160529T144500 +DTEND;TZID=Europe/Prague:20160529T154500 +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/plugins/plasma/pimeventsplugin/autotests/data/simple-oneday-event.json b/plugins/plasma/pimeventsplugin/autotests/data/simple-oneday-event.json new file mode 100644 index 0000000000000000000000000000000000000000..96f95236b35fc36f4861141c06f27e44f34b7d09 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/simple-oneday-event.json @@ -0,0 +1,25 @@ +{ + "rangeStart": "2016-05-01", + "rangeEnd": "2016-05-31", + "akonadiId": 6, + "eventData": [ + { + "summary": "Simple one day non-recurring event", + "description": "This is a simple one-day non-recurring event.", + "type": "Event", + "allDay": false, + "isMinor": false, + "startDateTime": { + "date": "2016-05-29", + "time": "14:45:00", + "tz": "Europe/Prague" + }, + "endDateTime": { + "date": "2016-05-29", + "time": "15:45:00", + "tz": "Europe/Prague" + }, + "uid": "Akonadi-6" + } + ] +} diff --git a/plugins/plasma/pimeventsplugin/autotests/data/simple-recurring-multiday-event.ics b/plugins/plasma/pimeventsplugin/autotests/data/simple-recurring-multiday-event.ics new file mode 100644 index 0000000000000000000000000000000000000000..3c904d7a4eba7235ad3c6362f2471e03f16cfb8d --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/simple-recurring-multiday-event.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTAMP:20160529T143526Z +CREATED:20160529T143451Z +UID:79146b91-fe07-40ec-ba16-402b4b94f655 +LAST-MODIFIED:20160529T143526Z +SUMMARY:Simple multi-day recurring event +DESCRIPTION:This is a simple multi-day recurring event +RRULE:FREQ=WEEKLY;BYDAY=FR +DTSTART;TZID=Europe/Prague:20160408T190000 +DTEND;TZID=Europe/Prague:20160410T120000 +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/plugins/plasma/pimeventsplugin/autotests/data/simple-recurring-multiday-event.json b/plugins/plasma/pimeventsplugin/autotests/data/simple-recurring-multiday-event.json new file mode 100644 index 0000000000000000000000000000000000000000..54d636044f185233c87fa072865a9de9899a8a00 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/simple-recurring-multiday-event.json @@ -0,0 +1,82 @@ +{ + "rangeStart": "2016-05-01", + "rangeEnd": "2016-05-31", + "akonadiId": 7, + "eventData": [ + { + "summary": "Simple multi-day recurring event", + "description": "This is a simple multi-day recurring event", + "type": "Event", + "allDay": false, + "isMinor": false, + "startDateTime": { + "date": "2016-05-06", + "time": "19:00:00", + "tz": "Europe/Prague" + }, + "endDateTime": { + "date": "2016-05-08", + "time": "12:00:00", + "tz": "Europe/Prague" + }, + "uid": "Akonadi-7-20160506T190000CEST" + }, + + { + "summary": "Simple multi-day recurring event", + "description": "This is a simple multi-day recurring event", + "type": "Event", + "allDay": false, + "isMinor": false, + "startDateTime": { + "date": "2016-05-13", + "time": "19:00:00", + "tz": "Europe/Prague" + }, + "endDateTime": { + "date": "2016-05-15", + "time": "12:00:00", + "tz": "Europe/Prague" + }, + "uid": "Akonadi-7-20160513T190000CEST" + }, + + { + "summary": "Simple multi-day recurring event", + "description": "This is a simple multi-day recurring event", + "type": "Event", + "allDay": false, + "isMinor": false, + "startDateTime": { + "date": "2016-05-20", + "time": "19:00:00", + "tz": "Europe/Prague" + }, + "endDateTime": { + "date": "2016-05-22", + "time": "12:00:00", + "tz": "Europe/Prague" + }, + "uid": "Akonadi-7-20160520T190000CEST" + }, + + { + "summary": "Simple multi-day recurring event", + "description": "This is a simple multi-day recurring event", + "type": "Event", + "allDay": false, + "isMinor": false, + "startDateTime": { + "date": "2016-05-27", + "time": "19:00:00", + "tz": "Europe/Prague" + }, + "endDateTime": { + "date": "2016-05-29", + "time": "12:00:00", + "tz": "Europe/Prague" + }, + "uid": "Akonadi-7-20160527T190000CEST" + } + ] +} diff --git a/plugins/plasma/pimeventsplugin/autotests/data/simple-recurring-oneday-event.ics b/plugins/plasma/pimeventsplugin/autotests/data/simple-recurring-oneday-event.ics new file mode 100644 index 0000000000000000000000000000000000000000..9f77fa69b895f615d45a9ea2999326ec131c3451 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/simple-recurring-oneday-event.ics @@ -0,0 +1,15 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTAMP:20160529T143213Z +CREATED:20160529T143106Z +UID:69971015-fe9c-4800-a20e-d46bafa24e41 +SEQUENCE:2 +LAST-MODIFIED:20160529T143213Z +DESCRIPTION:This is a simple one-day recurring event +SUMMARY:Simple one-day recurring event +RRULE:FREQ=WEEKLY;BYDAY=FR +DTSTART;TZID=Europe/Prague:20160401T141500 +DTEND;TZID=Europe/Prague:20160401T151500 +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/plugins/plasma/pimeventsplugin/autotests/data/simple-recurring-oneday-event.json b/plugins/plasma/pimeventsplugin/autotests/data/simple-recurring-oneday-event.json new file mode 100644 index 0000000000000000000000000000000000000000..b33c47e4f054295c531ce02f0b93adf1acd4d80f --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/data/simple-recurring-oneday-event.json @@ -0,0 +1,82 @@ +{ + "rangeStart": "2016-05-01", + "rangeEnd": "2016-05-31", + "akonadiId": 8, + "eventData": [ + { + "summary": "Simple one-day recurring event", + "description": "This is a simple one-day recurring event", + "type": "Event", + "allDay": false, + "isMinor": false, + "startDateTime": { + "date": "2016-05-06", + "time": "14:15:00", + "tz": "Europe/Prague" + }, + "endDateTime": { + "date": "2016-05-06", + "time": "15:15:00", + "tz": "Europe/Prague" + }, + "uid": "Akonadi-8-20160506T141500CEST" + }, + + { + "summary": "Simple one-day recurring event", + "description": "This is a simple one-day recurring event", + "type": "Event", + "allDay": false, + "isMinor": false, + "startDateTime": { + "date": "2016-05-13", + "time": "14:15:00", + "tz": "Europe/Prague" + }, + "endDateTime": { + "date": "2016-05-13", + "time": "15:15:00", + "tz": "Europe/Prague" + }, + "uid": "Akonadi-8-20160513T141500CEST" + }, + + { + "summary": "Simple one-day recurring event", + "description": "This is a simple one-day recurring event", + "type": "Event", + "allDay": false, + "isMinor": false, + "startDateTime": { + "date": "2016-05-20", + "time": "14:15:00", + "tz": "Europe/Prague" + }, + "endDateTime": { + "date": "2016-05-20", + "time": "15:15:00", + "tz": "Europe/Prague" + }, + "uid": "Akonadi-8-20160520T141500CEST" + }, + + { + "summary": "Simple one-day recurring event", + "description": "This is a simple one-day recurring event", + "type": "Event", + "allDay": false, + "isMinor": false, + "startDateTime": { + "date": "2016-05-27", + "time": "14:15:00", + "tz": "Europe/Prague" + }, + "endDateTime": { + "date": "2016-05-27", + "time": "15:15:00", + "tz": "Europe/Prague" + }, + "uid": "Akonadi-8-20160527T141500CEST" + } + ] +} diff --git a/plugins/plasma/pimeventsplugin/autotests/eventdatavisitortest.cpp b/plugins/plasma/pimeventsplugin/autotests/eventdatavisitortest.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9e1ac6b4c4fed801d7ddb6b62ee3e5a24078041e --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/eventdatavisitortest.cpp @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 "eventdatavisitortest.h" +#include "testdataparser.h" +#include "fakepimdatasource.h" +#include "testutils.h" +#include "../eventdatavisitor.h" + +#include + +#include +#include +#include + +#include + +Q_DECLARE_METATYPE(CalendarEvents::EventData) +Q_DECLARE_METATYPE(KCalCore::Incidence::Ptr) + +template +class TestableVisitor : public Visitor +{ +public: + TestableVisitor(PimDataSource *source, const QDate &start = QDate(), const QDate &end = QDate()) + : Visitor(source, start, end) + { + } + + QString callGenerateUid(const KCalCore::Incidence::Ptr &incidence, + const KDateTime &recurrenceId) const + { + return Visitor::generateUid(incidence, recurrenceId); + } + + bool callIsInRange(const QDate &start, const QDate &end) const + { + return Visitor::isInRange(start, end); + } + + QVector callExplodeIncidenceOccurences(const CalendarEvents::EventData &baseEd, + const KCalCore::Incidence::Ptr &incidence, + bool &ok) + { + return Visitor::explodeIncidenceOccurences(baseEd, incidence, ok); + } +}; + +using TestableEventDataVisitor = TestableVisitor; +using TestableEventDataIdVisitor = TestableVisitor; + +using DateTimeRange = QPair; + +void EventDataVisitorTest::testGenerateUID_data() +{ + QTest::addColumn("incidence"); + QTest::addColumn("recurrenceId"); + QTest::addColumn("itemId"); + QTest::addColumn("expectedUID"); + + auto incidence = KCalCore::Event::Ptr::create().staticCast(); + QTest::newRow("simple event") << incidence << KDateTime() + << 1ll << QStringLiteral("Akonadi-1"); + QTest::newRow("recurring event") << incidence << KDateTime(QDate(2016, 5, 29), QTime(15, 47, 0), KDateTime::UTC) + << 1ll << QStringLiteral("Akonadi-1-20160529T154700UTC"); + + incidence = KCalCore::Todo::Ptr::create().staticCast(); + QTest::newRow("simple todo") << incidence << KDateTime() + << 42ll << QStringLiteral("Akonadi-42"); + QTest::newRow("recurring todo") << incidence << KDateTime(QDate(2016, 5, 29), QTime(15, 49, 5), KDateTime::UTC) + << 42ll << QStringLiteral("Akonadi-42-20160529T154905UTC"); +} + + +void EventDataVisitorTest::testGenerateUID() +{ + QFETCH(KCalCore::Incidence::Ptr, incidence); + QFETCH(KDateTime, recurrenceId); + QFETCH(qint64, itemId); + QFETCH(QString, expectedUID); + + FakePimDataSource source; + source.setAkonadiIdForIncidence(incidence, itemId); + TestableEventDataVisitor visitor(&source); + + const QString result = visitor.callGenerateUid(incidence, recurrenceId); + QCOMPARE(result, expectedUID); +} + + + +void EventDataVisitorTest::testIsInRange_data() +{ + QTest::addColumn("rangeStart"); + QTest::addColumn("rangeEnd"); + QTest::addColumn("eventStart"); + QTest::addColumn("eventEnd"); + QTest::addColumn("expectedResult"); + + QTest::newRow("single day fully in-range") << QDate(2016, 5, 1) << QDate(2016, 5, 31) + << QDate(2016, 5, 3) << QDate(2016, 5, 3) + << true; + QTest::newRow("multiday fully in-range") << QDate(2016, 5, 1) << QDate(2016, 5, 31) + << QDate(2016, 5, 3) << QDate(2016, 5, 15) + << true; + QTest::newRow("multiday start overlap") << QDate(2016, 5, 1) << QDate(2016, 5, 31) + << QDate(2016, 4, 28) << QDate(2016, 5, 5) + << true; + QTest::newRow("multiday end overlap") << QDate(2016, 5, 1) << QDate(2016, 5, 31) + << QDate(2016, 5, 28) << QDate(2016, 6, 5) + << true; + QTest::newRow("single day range edge start") << QDate(2016, 5, 1) << QDate(2016, 5, 31) + << QDate(2016, 5, 1) << QDate(2016, 5, 1) + << true; + QTest::newRow("single day range edge end") << QDate(2016, 5, 1) << QDate(2016, 5, 31) + << QDate(2016, 5, 31) << QDate(2016, 5, 31) + << true; + QTest::newRow("multiday range edge start") << QDate(2016, 5, 1) << QDate(2016, 5, 31) + << QDate(2016, 5, 1) << QDate(2016, 5, 10) + << true; + QTest::newRow("multiday range edge end") << QDate(2016, 5, 1) << QDate(2016, 5, 31) + << QDate(2016, 5, 20) << QDate(2016, 5, 31) + << true; + QTest::newRow("single day before range") << QDate(2016, 5, 1) << QDate(2016, 5, 31) + << QDate(2016, 4, 28) << QDate(2016, 4, 28) + << false; + QTest::newRow("single day after range") << QDate(2016, 5, 1) << QDate(2016, 5, 31) + << QDate(2016, 6, 4) << QDate(2016, 6, 4) + << false; + QTest::newRow("multiday before range") << QDate(2016, 5, 1) << QDate(2016, 5, 31) + << QDate(2016, 4, 12) << QDate(2016, 4, 20) + << false; + QTest::newRow("multiday after range") << QDate(2016, 5, 1) << QDate(2016, 5, 31) + << QDate(2016, 6, 5) << QDate(2016, 6, 10) + << false; +} + +void EventDataVisitorTest::testIsInRange() +{ + QFETCH(QDate, rangeStart); + QFETCH(QDate, rangeEnd); + QFETCH(QDate, eventStart); + QFETCH(QDate, eventEnd); + QFETCH(bool, expectedResult); + + FakePimDataSource source; + TestableEventDataVisitor visitor(&source, rangeStart, rangeEnd); + const bool result = visitor.callIsInRange(eventStart, eventEnd); + QCOMPARE(result, expectedResult); +} + + + +void EventDataVisitorTest::testExplodeIncidenceOccurences_data() +{ + QTest::addColumn("rangeStart"); + QTest::addColumn("rangeEnd"); + QTest::addColumn("baseEventData"); + QTest::addColumn("incidence"); + QTest::addColumn("akonadiItemId"); + QTest::addColumn>("expectedEventData"); + + const auto allTestData = TestDataParser::allTestData(); + for (const auto &testData : allTestData) { + TestDataParser parser(testData, true); + // skip non-recurring testcases + if (!parser.incidence()->recurs()) { + continue; + } + QTest::newRow(qPrintable(testData)) << parser.rangeStart() + << parser.rangeEnd() + << parser.eventData().first() + << parser.incidence() + << parser.akonadiId() + << parser.eventData(); + } +} + + +void EventDataVisitorTest::testExplodeIncidenceOccurences() +{ + QFETCH(QDate, rangeStart); + QFETCH(QDate, rangeEnd); + QFETCH(CalendarEvents::EventData, baseEventData); + QFETCH(KCalCore::Incidence::Ptr, incidence); + QFETCH(qint64, akonadiItemId); + QFETCH(QVector, expectedEventData); + + FakePimDataSource source; + source.setAkonadiIdForIncidence(incidence, akonadiItemId); + TestableEventDataVisitor visitor(&source, rangeStart, rangeEnd); + bool ok = false; + const auto results = visitor.callExplodeIncidenceOccurences(baseEventData, incidence, ok); + QVERIFY(ok); + + QCOMPARE(results.size(), expectedEventData.size()); + for (int i = 0; i < results.size(); ++i) { + QVERIFY(TestUtils::compareEventData(results[i], expectedEventData[i])); + } +} + + + +void EventDataVisitorTest::testEventDataVisitor_data() +{ + QTest::addColumn("rangeStart"); + QTest::addColumn("rangeEnd"); + QTest::addColumn("incidence"); + QTest::addColumn("akonadiItemId"); + QTest::addColumn>("expectedResults"); + + const auto allTestData = TestDataParser::allTestData(); + for (const auto &testData : allTestData) { + TestDataParser parser(testData); + + QTest::newRow(qPrintable(testData)) << parser.rangeStart() + << parser.rangeEnd() + << parser.incidence() + << parser.akonadiId() + << parser.eventData(); + } +} + +void EventDataVisitorTest::testEventDataVisitor() +{ + QFETCH(QDate, rangeStart); + QFETCH(QDate, rangeEnd); + QFETCH(KCalCore::Incidence::Ptr, incidence); + QFETCH(qint64, akonadiItemId); + QFETCH(QVector, expectedResults); + + FakePimDataSource source; + source.setAkonadiIdForIncidence(incidence, akonadiItemId); + TestableEventDataVisitor visitor(&source, rangeStart, rangeEnd); + QVERIFY(visitor.act(incidence)); + + const auto &results = visitor.results(); + QCOMPARE(results.size(), expectedResults.size()); + + auto resultValues = results.values(); + std::sort(resultValues.begin(), resultValues.end(), std::less()); + for (int i = 0; i < resultValues.size(); ++i) { + const auto &result = resultValues[i]; + const auto &expectedResult = expectedResults[i]; + + QVERIFY(TestUtils::compareEventData(result, expectedResult)); + } +} + + + +void EventDataVisitorTest::testEventDataIdVisitor_data() +{ + QTest::addColumn("rangeStart"); + QTest::addColumn("rangeEnd"); + QTest::addColumn("incidence"); + QTest::addColumn("akonadiItemId"); + QTest::addColumn("expectedUids"); + + const auto allTestData = TestDataParser::allTestData(); + for (const auto &testData : allTestData) { + TestDataParser parser(testData, true); + QStringList uids; + Q_FOREACH (const auto &ed, parser.eventData()) { + uids.push_back(ed.uid()); + } + QTest::newRow(qPrintable(testData)) << parser.rangeStart() + << parser.rangeEnd() + << parser.incidence() + << parser.akonadiId() + << uids; + } +} + +void EventDataVisitorTest::testEventDataIdVisitor() +{ + QFETCH(QDate, rangeStart); + QFETCH(QDate, rangeEnd); + QFETCH(KCalCore::Incidence::Ptr, incidence); + QFETCH(qint64, akonadiItemId); + QFETCH(QStringList, expectedUids); + + FakePimDataSource source; + source.setAkonadiIdForIncidence(incidence, akonadiItemId); + TestableEventDataIdVisitor visitor(&source, rangeStart, rangeEnd); + QVERIFY(visitor.act(incidence)); + + auto results = visitor.results(); + + qSort(results); + qSort(expectedUids); + QCOMPARE(results, expectedUids); +} + + +QTEST_MAIN(EventDataVisitorTest) diff --git a/plugins/plasma/pimeventsplugin/autotests/eventdatavisitortest.h b/plugins/plasma/pimeventsplugin/autotests/eventdatavisitortest.h new file mode 100644 index 0000000000000000000000000000000000000000..61229ac2154218ef2337f9a80bd3c4daa4e98a8a --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/eventdatavisitortest.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 EVENTDATAVISITORTEST_H +#define EVENTDATAVISITORTEST_H + +#include + +namespace CalendarEvents +{ +class EventData; +} + +class EventDataVisitorTest : public QObject +{ + Q_OBJECT + + +private Q_SLOTS: + void testGenerateUID_data(); + void testGenerateUID(); + + void testIsInRange_data(); + void testIsInRange(); + + void testExplodeIncidenceOccurences_data(); + void testExplodeIncidenceOccurences(); + + void testEventDataVisitor_data(); + void testEventDataVisitor(); + + void testEventDataIdVisitor_data(); + void testEventDataIdVisitor(); +}; + +#endif diff --git a/plugins/plasma/pimeventsplugin/autotests/fakepimdatasource.cpp b/plugins/plasma/pimeventsplugin/autotests/fakepimdatasource.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a64076df39f539a411f85c7b726a604f1d57092c --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/fakepimdatasource.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 "fakepimdatasource.h" + +#include + +FakePimDataSource::FakePimDataSource() + : PimDataSource() + , mCalendar(new KCalCore::MemoryCalendar(KDateTime::UTC)) +{ +} + +FakePimDataSource::~FakePimDataSource() +{ + delete mCalendar; +} + +void FakePimDataSource::setAkonadiIdForIncidence(const KCalCore::Incidence::Ptr &incidence, qint64 akonadiId) +{ + mAkonadiIdMap.insert(incidence, akonadiId); +} + +qint64 FakePimDataSource::akonadiIdForIncidence(const KCalCore::Incidence::Ptr &incidence) const +{ + return mAkonadiIdMap.value(incidence, -1); +} + +KCalCore::Calendar *FakePimDataSource::calendar() const +{ + return mCalendar; +} + +QString FakePimDataSource::calendarColorForIncidence(const KCalCore::Incidence::Ptr &) const +{ + return QString(); +} diff --git a/plugins/plasma/pimeventsplugin/autotests/fakepimdatasource.h b/plugins/plasma/pimeventsplugin/autotests/fakepimdatasource.h new file mode 100644 index 0000000000000000000000000000000000000000..5c1fbda83c5f94630927bb516614589b40a50e15 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/fakepimdatasource.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 FAKEPIMDATASOURCE_H +#define FAKEPIMDATASOURCE_H + +#include "pimdatasource.h" + +class FakePimDataSource : public PimDataSource +{ +public: + FakePimDataSource(); + ~FakePimDataSource(); + + void setAkonadiIdForIncidence(const KCalCore::Incidence::Ptr &incidence, qint64 akonadiId); + qint64 akonadiIdForIncidence(const KCalCore::Incidence::Ptr &incidence) const Q_DECL_OVERRIDE; + + KCalCore::Calendar *calendar() const Q_DECL_OVERRIDE; + + QString calendarColorForIncidence(const KCalCore::Incidence::Ptr &incidence) const Q_DECL_OVERRIDE; + +private: + QMap mAkonadiIdMap; + KCalCore::Calendar *mCalendar; +}; + + +#endif diff --git a/plugins/plasma/pimeventsplugin/autotests/pimeventsplugintest.cpp b/plugins/plasma/pimeventsplugin/autotests/pimeventsplugintest.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4e9c1137acc450f3dceaad2ce0add4c57bc87b6d --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/pimeventsplugintest.cpp @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 "pimeventsplugintest.h" +#include "fakepimdatasource.h" +#include "testdataparser.h" +#include "../pimeventsplugin.h" +#include "testutils.h" + +#include +#include + +Q_DECLARE_METATYPE(DateEventDataHash) +Q_DECLARE_METATYPE(CalendarEvents::EventData); + +void PimEventsPluginTest::initTestCase() +{ + qRegisterMetaType("QMultiHash"); + qRegisterMetaType("CalendarEvents::EventData"); +} + +bool PimEventsPluginTest::compareEventDataHashes(const DateEventDataHash &actual, + const DateEventDataHash &expected) +{ + COMPARE(actual.size(), expected.size()); + Q_FOREACH (const QDate &resultKey, actual.uniqueKeys()) { + VERIFY(expected.contains(resultKey)); + auto resultValues = actual.values(resultKey); + auto expectedValues = expected.values(resultKey); + COMPARE(resultValues.size(), expectedValues.size()); + std::sort(resultValues.begin(), resultValues.end(), std::less()); + std::sort(expectedValues.begin(), expectedValues.end(), std::less()); + COMPARE(resultValues, expectedValues); + } + return true; +} + +DateEventDataHash PimEventsPluginTest::populateCalendar(FakePimDataSource *source, bool uniqueEventData) +{ + const QStringList allData = TestDataParser::allTestData(); + DateEventDataHash expectedData; + Q_FOREACH (const QString &data, allData) { + TestDataParser parser(data, true); + if (parser.rangeEnd() < QDate(2016, 5, 1) || parser.rangeStart() > QDate(2016, 5, 31)) { + continue; + } + const KCalCore::Event::Ptr event = parser.incidence().dynamicCast(); + if (event) { + source->setAkonadiIdForIncidence(event, parser.akonadiId()); + source->calendar()->addEvent(event); + Q_FOREACH (const CalendarEvents::EventData &dt, parser.eventData()) { + if (uniqueEventData) { + expectedData.insert(dt.startDateTime().date(), dt); + } else { + QDate d = dt.startDateTime().date(); + while (d <= dt.endDateTime().date()) { + expectedData.insert(d, dt); + d = d.addDays(1); + } + } + } + } + } + + return expectedData; +} + +QVector PimEventsPluginTest::findEventData(const KCalCore::Event::Ptr &event, + const DateEventDataHash &allData) +{ + QVector data; + for (auto it = allData.cbegin(), end = allData.cend(); it != end; ++it) { + // This is a very naive check + if (it->title() == event->summary() + && it->description() == event->description() + && it->isAllDay() == event->allDay()) { + data.push_back((*it)); + } + } + + return data; +} + + + + +void PimEventsPluginTest::testLoadEventsForDataRange() +{ + FakePimDataSource source; + const DateEventDataHash expectedData = populateCalendar(&source, false); + + PimEventsPlugin plugin(&source); + QSignalSpy dataReadySpy(&plugin, &PimEventsPlugin::dataReady); + QVERIFY(dataReadySpy.isValid()); + + plugin.loadEventsForDateRange(QDate(2016, 5, 1), QDate(2016, 5, 31)); + QCOMPARE(dataReadySpy.size(), 1); + const auto results = dataReadySpy.takeFirst().first().value(); + QVERIFY(compareEventDataHashes(results, expectedData)); + + plugin.loadEventsForDateRange(QDate(2016, 1, 1), QDate(2016, 1, 30)); + QCOMPARE(dataReadySpy.size(), 0); +} + +void PimEventsPluginTest::testEventAdded() +{ + const QStringList allData = TestDataParser::allTestData(); + + FakePimDataSource source; + + PimEventsPlugin plugin(&source); + QSignalSpy dataReadySpy(&plugin, &PimEventsPlugin::dataReady); + QVERIFY(dataReadySpy.isValid()); + + plugin.loadEventsForDateRange(QDate(2016, 5, 1), QDate(2016, 5, 31)); + QCOMPARE(dataReadySpy.size(), 0); + + + Q_FOREACH (const QString &data, allData) { + TestDataParser parser(data, true); + if (parser.rangeEnd() < QDate(2016, 5, 1) || parser.rangeStart() > QDate(2016, 5, 31)) { + continue; + } + const KCalCore::Event::Ptr event = parser.incidence().dynamicCast(); + if (event) { + source.setAkonadiIdForIncidence(event, parser.akonadiId()); + source.calendar()->addEvent(event); + DateEventDataHash expectedData; + Q_FOREACH (const CalendarEvents::EventData &dt, parser.eventData()) { + QDate d = dt.startDateTime().date(); + while (d <= dt.endDateTime().date()) { + expectedData.insert(d, dt); + d = d.addDays(1); + } + } + + QCOMPARE(dataReadySpy.size(), 1); + const auto results = dataReadySpy.takeFirst().first().value(); + QVERIFY(compareEventDataHashes(results, expectedData)); + } + } +} + +void PimEventsPluginTest::testEventModified() +{ + FakePimDataSource source; + + PimEventsPlugin plugin(&source); + QSignalSpy eventModifiedSpy(&plugin, &PimEventsPlugin::eventModified); + QVERIFY(eventModifiedSpy.isValid()); + + // Populate model + const auto allData = populateCalendar(&source, true); + + // We don't care about the result of this, we just need to have mStart and + // mEnd set + plugin.loadEventsForDateRange(QDate(2016, 5, 1), QDate(2016, 5, 31)); + + // Non-recurring event + { + QVERIFY(eventModifiedSpy.isEmpty()); + KCalCore::Event::Ptr event = source.calendar()->event(QStringLiteral("4d7fdd2c-2d3a-4ecf-8964-00eb92225209")); + QVERIFY(event); + const auto expectedData = findEventData(event, allData); + QCOMPARE(expectedData.size(), 1); + event->setSummary(QStringLiteral("TEST")); + + QVERIFY(source.calendar()->addEvent(event)); + + QCOMPARE(eventModifiedSpy.size(), 1); + const auto result = eventModifiedSpy.takeFirst().first().value(); + // TODO: Test for other property changes too? + QCOMPARE(result.title(), event->summary()); + QCOMPARE(result.uid(), expectedData[0].uid()); + } + + // Recurring event + { + QVERIFY(eventModifiedSpy.isEmpty()); + KCalCore::Event::Ptr event = source.calendar()->event(QStringLiteral("69971015-fe9c-4800-a20e-d46bafa24e41")); + QVERIFY(event); + auto expectedData = findEventData(event, allData); + event->setSummary(QStringLiteral("TEST2")); + QVERIFY(source.calendar()->addEvent(event)); + + QCOMPARE(eventModifiedSpy.size(), expectedData.size()); + while (!eventModifiedSpy.isEmpty()) { + const auto args = eventModifiedSpy.takeFirst(); + const auto &resultData = args[0].value(); + const auto expected = std::find_if(expectedData.begin(), expectedData.end(), + [resultData](const CalendarEvents::EventData &e) { + return e.uid() == resultData.uid(); + }); + QVERIFY(expected != expectedData.end()); + expectedData.erase(expected); + QCOMPARE(resultData.title(), QString::fromLatin1("TEST2")); + } + QVERIFY(expectedData.isEmpty()); + } +} + +void PimEventsPluginTest::testEventRemoved() +{ + FakePimDataSource source; + + PimEventsPlugin plugin(&source); + QSignalSpy eventRemovedSpy(&plugin, &PimEventsPlugin::eventRemoved); + QVERIFY(eventRemovedSpy.isValid()); + + const auto allData = populateCalendar(&source, true); + + // We don't care about the result of this, we just need to have mStart and + // mEnd set + plugin.loadEventsForDateRange(QDate(2016, 5, 1), QDate(2016, 5, 31)); + + // Non-recurring event + { + QVERIFY(eventRemovedSpy.isEmpty()); + const KCalCore::Event::Ptr event = source.calendar()->event(QStringLiteral("4d7fdd2c-2d3a-4ecf-8964-00eb92225209")); + QVERIFY(event); + const auto expectedData = findEventData(event, allData); + QCOMPARE(expectedData.size(), 1); + + QVERIFY(source.calendar()->deleteEvent(event)); + + QCOMPARE(eventRemovedSpy.size(), 1); + const auto result = eventRemovedSpy.takeFirst().first().toString(); + QCOMPARE(result, expectedData[0].uid()); + } + + // Recurring event + { + QVERIFY(eventRemovedSpy.isEmpty()); + KCalCore::Event::Ptr event = source.calendar()->event(QStringLiteral("69971015-fe9c-4800-a20e-d46bafa24e41")); + QVERIFY(event); + auto expectedData = findEventData(event, allData); + + QVERIFY(source.calendar()->deleteEvent(event)); + + QCOMPARE(eventRemovedSpy.size(), expectedData.size()); + while (!eventRemovedSpy.isEmpty()) { + const QString resultUid = eventRemovedSpy.takeFirst().first().toString(); + const auto expected = std::find_if(expectedData.begin(), expectedData.end(), + [resultUid](const CalendarEvents::EventData &e) { + return e.uid() == resultUid; + }); + QVERIFY(expected != expectedData.end()); + expectedData.erase(expected); + } + QVERIFY(expectedData.isEmpty()); + } +} + + +QTEST_MAIN(PimEventsPluginTest) diff --git a/plugins/plasma/pimeventsplugin/autotests/pimeventsplugintest.h b/plugins/plasma/pimeventsplugin/autotests/pimeventsplugintest.h new file mode 100644 index 0000000000000000000000000000000000000000..09e5ef948ddc7d83503f70ccfb788565e65b1b78 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/pimeventsplugintest.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 PIMEVENTSPLUGINTEST_H +#define PIMEVENTSPLUGINTEST_H + +#include +#include +#include + +class QDate; +namespace CalendarEvents { +class EventData; +} +class FakePimDataSource; + +using DateEventDataHash = QMultiHash; + +class PimEventsPluginTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void testLoadEventsForDataRange(); + void testEventAdded(); + void testEventModified(); + void testEventRemoved(); + +private: + bool compareEventDataHashes(const DateEventDataHash &actual, + const DateEventDataHash &expected); + DateEventDataHash populateCalendar(FakePimDataSource *source, bool uniqueEventData); + QVector findEventData(const KCalCore::Event::Ptr &event, + const DateEventDataHash &allData); +}; + +#endif diff --git a/plugins/plasma/pimeventsplugin/autotests/testdataparser.cpp b/plugins/plasma/pimeventsplugin/autotests/testdataparser.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a50a193940cd81a585ec8f7af7a74637d9a015de --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/testdataparser.cpp @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 "testdataparser.h" +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +TestDataParser::TestDataParser(const QString &testData, bool uniqueEventData) + : mTestData(testData) + , mUniqueEventData(uniqueEventData) +{ + parse(); +} + +TestDataParser::~TestDataParser() +{ +} + +QStringList TestDataParser::allTestData() +{ + QDir testdir(QStringLiteral(QT_TESTCASE_BUILDDIR "/data")); + const auto data = testdir.entryInfoList({ QStringLiteral("*.json") }, QDir::Files); + QStringList testcases; + for (const auto &fi : data) { + testcases << fi.baseName(); + } + return testcases; +} + +QDate TestDataParser::rangeStart() const +{ + return mRangeStart; +} + +QDate TestDataParser::rangeEnd() const +{ + return mRangeEnd; +} + +qint64 TestDataParser::akonadiId() const +{ + return mAkonadiId; +} + +QVector TestDataParser::eventData() const +{ + return mEventData; +} + +KCalCore::Incidence::Ptr TestDataParser::incidence() const +{ + return mIncidence; +} + +QDateTime TestDataParser::parseDateTime(const QJsonObject &dateTime) +{ + return QDateTime(QDate::fromString(dateTime[QStringLiteral("date")].toString(), Qt::ISODate), + QTime::fromString(dateTime[QStringLiteral("time")].toString(), Qt::ISODate), + QTimeZone(dateTime[QStringLiteral("tz")].toString().toLatin1())); +} + +void TestDataParser::parse() +{ + QFile icalFile(QStringLiteral(QT_TESTCASE_BUILDDIR "/data/%1.ics").arg(mTestData)); + QVERIFY(icalFile.exists()); + QVERIFY(icalFile.open(QIODevice::ReadOnly)); + + const QByteArray data = icalFile.readAll(); + KCalCore::ICalFormat format; + mIncidence = format.readIncidence(data); + QVERIFY(mIncidence); + + QFile jsonFile(QStringLiteral(QT_TESTCASE_BUILDDIR "/data/%1.json").arg(mTestData)); + QVERIFY(jsonFile.exists()); + QVERIFY(jsonFile.open(QIODevice::ReadOnly)); + + const QByteArray json = jsonFile.readAll(); + const QJsonDocument jsonDoc = QJsonDocument::fromJson(json); + const QJsonObject doc = jsonDoc.object(); + mRangeStart = QDate::fromString(doc[QStringLiteral("rangeStart")].toString(), Qt::ISODate); + mRangeEnd = QDate::fromString(doc[QStringLiteral("rangeEnd")].toString(), Qt::ISODate); + mAkonadiId = doc[QStringLiteral("akonadiId")].toInt(); + + const QJsonArray array = doc[QStringLiteral("eventData")].toArray(); + for (auto iter = array.constBegin(), end = array.constEnd(); iter != end; ++iter) { + CalendarEvents::EventData eventData; + const QJsonObject obj = iter->toObject(); + eventData.setTitle(obj[QStringLiteral("summary")].toString()); + eventData.setDescription(obj[QStringLiteral("description")].toString()); + const QString type = obj[QStringLiteral("type")].toString(); + if (type == QLatin1String("Event")) { + eventData.setEventType(CalendarEvents::EventData::Event); + } else { + eventData.setEventType(CalendarEvents::EventData::Todo); + } + eventData.setIsAllDay(obj[QStringLiteral("allDay")].toBool()); + eventData.setIsMinor(obj[QStringLiteral("isMinor")].toBool()); + const QDateTime startDateTime = parseDateTime(obj[QStringLiteral("startDateTime")].toObject()); + eventData.setStartDateTime(startDateTime); + const QDateTime endDateTime = parseDateTime(obj[QStringLiteral("endDateTime")].toObject()); + eventData.setEndDateTime(endDateTime); + eventData.setUid(obj[QStringLiteral("uid")].toString()); + + if (mUniqueEventData) { + mEventData.push_back(eventData); + } else { + QDate d = startDateTime.date(); + const QDate dateEnd = endDateTime.date(); + while (d <= dateEnd) { + mEventData.push_back(eventData); + d = d.addDays(1); + } + } + } + QVERIFY(!mEventData.isEmpty()); +} diff --git a/plugins/plasma/pimeventsplugin/autotests/testdataparser.h b/plugins/plasma/pimeventsplugin/autotests/testdataparser.h new file mode 100644 index 0000000000000000000000000000000000000000..18eb5c36eb192f72077803bfe9a2128f31e30c25 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/testdataparser.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 + +namespace CalendarEvents +{ +class EventData; +} +class QDateTime; +class QJsonObject; + +class TestDataParser +{ +public: + explicit TestDataParser(const QString &testData, bool uniqueEventData = false); + ~TestDataParser(); + + QDate rangeStart() const; + QDate rangeEnd() const; + qint64 akonadiId() const; + KCalCore::Incidence::Ptr incidence() const; + QVector eventData() const; + + static QStringList allTestData(); + +private: + void parse(); + QDateTime parseDateTime(const QJsonObject &datetime); + + QString mTestData; + QDate mRangeStart; + QDate mRangeEnd; + qint64 mAkonadiId; + KCalCore::Incidence::Ptr mIncidence; + QVector mEventData; + bool mUniqueEventData; +}; diff --git a/plugins/plasma/pimeventsplugin/autotests/testutils.h b/plugins/plasma/pimeventsplugin/autotests/testutils.h new file mode 100644 index 0000000000000000000000000000000000000000..71f698098a545378c433557a130d8815457985ed --- /dev/null +++ b/plugins/plasma/pimeventsplugin/autotests/testutils.h @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 TESTUTILS_H +#define TESTUTILS_H + +#include +#include +#include + +QDebug operator<<(QDebug dbg, const CalendarEvents::EventData &data) +{ + dbg.nospace() << data.title() << " (UID " << data.uid() << "), " + << data.startDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss t")) + << " - " + << data.endDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss t")); + return dbg; +} + +bool operator==(const CalendarEvents::EventData &lhs, const CalendarEvents::EventData &rhs) +{ + return lhs.uid() == rhs.uid() + && lhs.type() == rhs.type() + && lhs.isAllDay() == rhs.isAllDay() + && lhs.isMinor() == rhs.isMinor() + && lhs.title() == rhs.title() + && lhs.startDateTime() == rhs.startDateTime() + && lhs.endDateTime() == rhs.endDateTime() + && lhs.description() == rhs.description() + && lhs.eventColor() == rhs.eventColor(); +} + +namespace std +{ +bool operator<(const CalendarEvents::EventData &lhs, const CalendarEvents::EventData &rhs) +{ + if (lhs.startDateTime() != rhs.startDateTime()) { + return lhs.startDateTime() < rhs.startDateTime(); + } else if (lhs.endDateTime() != rhs.endDateTime()) { + return lhs.endDateTime() < rhs.endDateTime(); + } else { + return lhs.uid() < rhs.uid(); + } +} +} + +#define COMPARE(_actual, _expected) \ +{ \ + bool ok = false; \ + [&]() { \ + QCOMPARE(_actual, _expected); \ + ok = true; \ + }(); \ + if (!ok) { \ + return false; \ + } \ +} + +#define VERIFY(_cond) \ +{ \ + bool ok = false; \ + [&]() { \ + QVERIFY(_cond); \ + ok = true; \ + }(); \ + if (!ok) { \ + return false; \ + } \ +} + +namespace TestUtils +{ + +bool compareEventData(const CalendarEvents::EventData &actual, + const CalendarEvents::EventData &expected) +{ + COMPARE(actual.title(), expected.title()); + COMPARE(actual.description(), expected.description()); + COMPARE(actual.isAllDay(), expected.isAllDay()); + COMPARE(actual.isMinor(), expected.isMinor()); + COMPARE(actual.type(), expected.type()); + COMPARE(actual.eventColor(), expected.eventColor()); + COMPARE(actual.uid(), expected.uid()); + COMPARE(actual.startDateTime(), expected.startDateTime()); + COMPARE(actual.endDateTime(), expected.endDateTime()); + + return true; +} + +} + + +#endif diff --git a/plugins/plasma/pimeventsplugin/eventdatavisitor.cpp b/plugins/plasma/pimeventsplugin/eventdatavisitor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..50b946f37f65c36cb586a055024f6f2ab908fd63 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/eventdatavisitor.cpp @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 "eventdatavisitor.h" +#include "pimdatasource.h" +#include "pimeventsplugin_debug.h" + +BaseEventDataVisitor::BaseEventDataVisitor(PimDataSource *dataSource, + const QDate &start, const QDate &end) + : mDataSource(dataSource) + , mStart(start) + , mEnd(end) +{ +} + +BaseEventDataVisitor::~BaseEventDataVisitor() +{ +} + +bool BaseEventDataVisitor::act(const KCalCore::Incidence::Ptr &incidence) +{ + return incidence->accept(*this, incidence); +} + +bool BaseEventDataVisitor::act(const KCalCore::Event::List &events) +{ + bool ok = false; + Q_FOREACH (const KCalCore::Event::Ptr &event, events) { + ok = event.staticCast()->accept(*this, event) || ok; + } + return ok; +} +bool BaseEventDataVisitor::act(const KCalCore::Todo::List &todos) +{ + bool ok = false; + Q_FOREACH (const KCalCore::Todo::Ptr &todo, todos) { + ok = todo.staticCast()->accept(*this, todo) || ok; + } + return ok; +} + +bool BaseEventDataVisitor::isInRange(const QDate &start, const QDate &end) const +{ + if (!mStart.isValid() || !mEnd.isValid()) { + return true; + } + + if (!end.isValid() && start >= mStart && start <= mEnd) { + return true; + } else if (start < mStart) { + return end >= mStart; + } else if (end > mEnd) { + return start <= mEnd; + } else { + return true; + } +} + +QString BaseEventDataVisitor::generateUid(const KCalCore::Incidence::Ptr &incidence, + const KDateTime &recurrenceId) const +{ + // Get a corresponding Akonadi Item: Akonadi ID is the only reliably unique + // and persistent identifier when dealing with incidences from multiple + // calendars + const qint64 itemId = mDataSource->akonadiIdForIncidence(incidence); + if (itemId <= 0) { + // Can this happen? What do we do now?! + return QString(); + } + + if (recurrenceId.isValid()) { + return QStringLiteral("Akonadi-%1-%2").arg(itemId) + .arg(recurrenceId.toString(QStringLiteral("%Y%m%dT%H%M%S%Z"))); + } else { + return QStringLiteral("Akonadi-%1").arg(itemId); + } +} + +QVector BaseEventDataVisitor::explodeIncidenceOccurences(const CalendarEvents::EventData &ed, + const KCalCore::Incidence::Ptr &incidence, + bool &ok) +{ + Q_ASSERT(incidence->recurs()); + + const qint64 duration = ed.startDateTime().secsTo(ed.endDateTime()); + + KDateTime rec(mStart.addDays(-1), QTime(0, 0, 0)); + rec = incidence->recurrence()->getNextDateTime(rec); + QVector results; + while (rec.isValid() && rec.date() <= mEnd) { + CalendarEvents::EventData copy = ed; + const QDateTime dt = rec.dateTime(); + copy.setStartDateTime(dt); + copy.setEndDateTime(dt.addSecs(duration)); + copy.setUid(generateUid(incidence, rec)); + results.push_back(copy); + + rec = incidence->recurrence()->getNextDateTime(rec); + } + + ok = true; + return results; +} + + + + +EventDataVisitor::EventDataVisitor(PimDataSource *dataSource, + const QDate &start, const QDate &end) + : BaseEventDataVisitor(dataSource, start , end) +{ +} + +EventDataVisitor::~EventDataVisitor() +{ +} + + +const QMultiHash &EventDataVisitor::results() const +{ + return mResults; +} + +bool EventDataVisitor::visit(const KCalCore::Incidence::Ptr &incidence, + CalendarEvents::EventData::EventType type) +{ + CalendarEvents::EventData data = incidenceData(incidence); + data.setEventType(type); + if (incidence->recurs()) { + bool ok = false; + const auto list = explodeIncidenceOccurences(data, incidence, ok); + if (ok) { + for (const auto &data : list) { + insertResult(data); + } + } + return ok; + } else if (isInRange(data.startDateTime().date(), data.endDateTime().date())) { + insertResult(data); + return true; + } + + return false; +} + +bool EventDataVisitor::visit(const KCalCore::Event::Ptr &event) +{ + return visit(event, CalendarEvents::EventData::Event); +} + +bool EventDataVisitor::visit(const KCalCore::Todo::Ptr &todo) +{ + return visit(todo, CalendarEvents::EventData::Todo); +} + +void EventDataVisitor::insertResult(const CalendarEvents::EventData &result) +{ + QDate d = result.startDateTime().date(); + const QDate end = result.endDateTime().date(); + while (d <= end) { + mResults.insert(d, result); + d = d.addDays(1); + } +} + + +CalendarEvents::EventData EventDataVisitor::incidenceData(const KCalCore::Incidence::Ptr &incidence) const +{ + CalendarEvents::EventData data; + data.setTitle(incidence->summary()); + data.setDescription(incidence->description()); + data.setIsAllDay(incidence->allDay()); + data.setIsMinor(false); + data.setUid(generateUid(incidence)); + data.setStartDateTime(incidence->dtStart().dateTime()); + data.setEndDateTime(incidence->dateTime(KCalCore::Incidence::RoleEnd).dateTime()); + data.setEventColor(mDataSource->calendarColorForIncidence(incidence)); + return data; +} + +EventDataIdVisitor::EventDataIdVisitor(PimDataSource *dataSource, const QDate &start, const QDate &end) + : BaseEventDataVisitor(dataSource, start, end) +{ +} + +const QStringList &EventDataIdVisitor::results() const +{ + return mResults; +} + +bool EventDataIdVisitor::visit(const KCalCore::Event::Ptr &event) +{ + return visit(event.staticCast()); +} + +bool EventDataIdVisitor::visit(const KCalCore::Todo::Ptr &todo) +{ + return visit(todo.staticCast()); +} + +bool EventDataIdVisitor::visit(const KCalCore::Incidence::Ptr& incidence) +{ + if (incidence->recurs()) { + CalendarEvents::EventData ed; + bool ok = false; + const auto list = explodeIncidenceOccurences(ed, incidence, ok); + if (ok) { + for (const auto &data : list) { + mResults.push_back(data.uid()); + } + } + return ok; + } else { + mResults.push_back(generateUid(incidence, incidence->recurrenceId())); + } + return true; +} diff --git a/plugins/plasma/pimeventsplugin/eventdatavisitor.h b/plugins/plasma/pimeventsplugin/eventdatavisitor.h new file mode 100644 index 0000000000000000000000000000000000000000..c6a43e5dbfdbcaacebd0c19d4b182294e03a6c15 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/eventdatavisitor.h @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 EVENTDATAVISITOR_H +#define EVENTDATAVISITOR_H + +#include +#include +#include + +class PimDataSource; +class BaseEventDataVisitor : public KCalCore::Visitor +{ +public: + ~BaseEventDataVisitor(); + + bool act(const KCalCore::Incidence::Ptr &incidence); + bool act(const KCalCore::Event::List &events); + bool act(const KCalCore::Todo::List &todos); + +protected: + BaseEventDataVisitor(PimDataSource *dataSource, const QDate &start, const QDate &end); + + QString generateUid(const KCalCore::Incidence::Ptr &incidence, const KDateTime &recurrenceId = KDateTime()) const; + bool isInRange(const QDate &start, const QDate &end) const; + + QVector explodeIncidenceOccurences(const CalendarEvents::EventData &ed, + const KCalCore::Incidence::Ptr &incidence, + bool &ok); +protected: + PimDataSource *mDataSource; + QDate mStart; + QDate mEnd; +}; + +class EventDataVisitor : public BaseEventDataVisitor +{ +public: + EventDataVisitor(PimDataSource *dataSource, const QDate &start, const QDate &end); + ~EventDataVisitor(); + + const QMultiHash &results() const; + +protected: + bool visit(const KCalCore::Event::Ptr &event) Q_DECL_OVERRIDE; + bool visit(const KCalCore::Todo::Ptr &todo) Q_DECL_OVERRIDE; + bool visit(const KCalCore::Journal::Ptr &) Q_DECL_OVERRIDE { return false; } + bool visit(const KCalCore::FreeBusy::Ptr &) Q_DECL_OVERRIDE { return false; } +private: + void insertResult(const CalendarEvents::EventData &result); + + bool visit(const KCalCore::Incidence::Ptr &incidence, CalendarEvents::EventData::EventType eventType); + CalendarEvents::EventData incidenceData(const KCalCore::Incidence::Ptr &incidence) const; + + QMultiHash mResults; + +}; + + +class EventDataIdVisitor : public BaseEventDataVisitor +{ +public: + explicit EventDataIdVisitor(PimDataSource *dataSource, const QDate &start, const QDate &end); + + const QStringList &results() const; + +protected: + bool visit(const KCalCore::Event::Ptr &event) Q_DECL_OVERRIDE; + bool visit(const KCalCore::Todo::Ptr &todo) Q_DECL_OVERRIDE; + bool visit(const KCalCore::Journal::Ptr &) Q_DECL_OVERRIDE { return false; } + bool visit(const KCalCore::FreeBusy::Ptr &) Q_DECL_OVERRIDE { return false; } + +private: + bool visit(const KCalCore::Incidence::Ptr &incidence); + + QStringList mResults; +}; + + +#endif diff --git a/plugins/plasma/pimeventsplugin/pimcalendarsmodel.cpp b/plugins/plasma/pimeventsplugin/pimcalendarsmodel.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e0fa92998cbbb2c7da0d7c7b91f5b9dbadb4ba6b --- /dev/null +++ b/plugins/plasma/pimeventsplugin/pimcalendarsmodel.cpp @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 "pimcalendarsmodel.h" +#include "settingschangenotifier.h" + +#include +#include +#include +#include + +#include +#include + +#include +#include + +PimCalendarsModel::PimCalendarsModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ + setSortRole(Qt::DisplayRole); + setSortCaseSensitivity(Qt::CaseInsensitive); + setSortLocaleAware(true); + setDynamicSortFilter(true); + + auto cr = new Akonadi::ChangeRecorder(this); + cr->setMimeTypeMonitored(KCalCore::Event::eventMimeType()); + cr->setMimeTypeMonitored(KCalCore::Todo::todoMimeType()); + cr->setTypeMonitored(Akonadi::Monitor::Collections); + cr->collectionFetchScope().setListFilter(Akonadi::CollectionFetchScope::Enabled); + + mEtm = new Akonadi::EntityTreeModel(cr); + mEtm->setItemPopulationStrategy(Akonadi::EntityTreeModel::NoItemPopulation); + mEtm->setListFilter(Akonadi::CollectionFetchScope::Enabled); + connect(mEtm, &Akonadi::EntityTreeModel::collectionTreeFetched, + this, [this]() { sort(0, Qt::AscendingOrder); }); + + setSourceModel(mEtm); + + auto config = KSharedConfig::openConfig(); + auto group = config->group("PIMEventsPlugin"); + mEnabledCalendars = QSet::fromList(group.readEntry(QStringLiteral("calendars"), QList())); +} + +PimCalendarsModel::~PimCalendarsModel() +{ +} +QHash PimCalendarsModel::roleNames() const +{ + return { { DataRole, "data" } }; +} + +QVariant PimCalendarsModel::data(const QModelIndex &index, int role) const +{ + if (role == Qt::DisplayRole) { + return QSortFilterProxyModel::data(index, role); + } + + if (role != DataRole) { + return QVariant(); + } + + const auto &col = QSortFilterProxyModel::data(index, Akonadi::EntityTreeModel::CollectionRole).value(); + if (!col.isValid()) { + return QVariant(); + } + const auto mts = col.contentMimeTypes(); + const bool enabled = mts.contains(KCalCore::Event::eventMimeType()) || mts.contains(KCalCore::Todo::todoMimeType()); + + auto attr = col.attribute(); + const QString icon = attr ? attr->iconName() : QString::null; + return QVariantMap{ { QStringLiteral("id"), col.id() }, + { QStringLiteral("name"), col.displayName() }, + { QStringLiteral("enabled"), enabled }, + { QStringLiteral("checked"), mEnabledCalendars.contains(col.id()) }, + { QStringLiteral("iconName"), icon } }; +} + +void PimCalendarsModel::setChecked(qint64 collectionId, bool checked) +{ + bool done = false; + if (checked) { + done = !mEnabledCalendars.contains(collectionId); + mEnabledCalendars.insert(collectionId); + } else { + done = mEnabledCalendars.remove(collectionId); + } + if (done) { + const QModelIndex idx = Akonadi::EntityTreeModel::modelIndexForCollection(this, Akonadi::Collection(collectionId)); + Q_EMIT dataChanged(idx, idx); + } +} + +void PimCalendarsModel::saveConfig() +{ + auto config = KSharedConfig::openConfig(); + auto group = config->group("PIMEventsPlugin"); + auto savedList = group.readEntry("calendars", QList()); + auto currentList = mEnabledCalendars.toList(); + qSort(savedList); + qSort(currentList); + + if (currentList != savedList) { + group.writeEntry("calendars", currentList); + SettingsChangeNotifier::self()->notifySettingsChanged(); + } +} + diff --git a/plugins/plasma/pimeventsplugin/pimcalendarsmodel.h b/plugins/plasma/pimeventsplugin/pimcalendarsmodel.h new file mode 100644 index 0000000000000000000000000000000000000000..344c71a6677197a75429b9e12178e09445f6183d --- /dev/null +++ b/plugins/plasma/pimeventsplugin/pimcalendarsmodel.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 PIMCALENDARSMODEL_H +#define PIMCALENDARSMODEL_H + +#include +#include + +namespace Akonadi { +class EntityTreeModel; +} + +class PimCalendarsModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + enum Roles { + DataRole = Qt::UserRole + 1 + }; + + explicit PimCalendarsModel(QObject *parent = Q_NULLPTR); + ~PimCalendarsModel(); + + QHash roleNames() const Q_DECL_OVERRIDE; + QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; + +public Q_SLOTS: + void setChecked(qint64 collectionId, bool checked); + void saveConfig(); + +private: + Akonadi::EntityTreeModel *mEtm; + QSet mEnabledCalendars; +}; + + +#endif diff --git a/plugins/plasma/pimeventsplugin/pimcalendarsplugin.cpp b/plugins/plasma/pimeventsplugin/pimcalendarsplugin.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c1375836c242eafded2d23239c160d876eb3be6c --- /dev/null +++ b/plugins/plasma/pimeventsplugin/pimcalendarsplugin.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 "pimcalendarsmodel.h" + +class PimCalendarsPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) Q_DECL_OVERRIDE + { + qmlRegisterType(uri, 1, 0, "PimCalendarsModel"); + } +}; + +#include "pimcalendarsplugin.moc" diff --git a/plugins/plasma/pimeventsplugin/pimdatasource.h b/plugins/plasma/pimeventsplugin/pimdatasource.h new file mode 100644 index 0000000000000000000000000000000000000000..b60ddd490e4242143bafab6f6ae5d4cc3b7d52de --- /dev/null +++ b/plugins/plasma/pimeventsplugin/pimdatasource.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 PIMDATASOURCE_H +#define PIMDATASOURCE_H + +#include + +namespace KCalCore { +class Calendar; +} + +class PimDataSource +{ +public: + virtual ~PimDataSource() {} + + virtual KCalCore::Calendar *calendar() const = 0; + virtual qint64 akonadiIdForIncidence(const KCalCore::Incidence::Ptr &incidence) const = 0; + virtual QString calendarColorForIncidence(const KCalCore::Incidence::Ptr &incidence) const = 0; +}; + +#endif diff --git a/plugins/plasma/pimeventsplugin/pimeventsplugin.cpp b/plugins/plasma/pimeventsplugin/pimeventsplugin.cpp new file mode 100644 index 0000000000000000000000000000000000000000..dea6d91a93350b0730ecfacb0f66c0427342b1de --- /dev/null +++ b/plugins/plasma/pimeventsplugin/pimeventsplugin.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 "pimeventsplugin.h" +#include "eventdatavisitor.h" +#include "akonadipimdatasource.h" +#include "pimeventsplugin_debug.h" + +PimEventsPlugin::PimEventsPlugin(QObject* parent) + : PimEventsPlugin(new AkonadiPimDataSource(), parent) +{ + static_cast(mDataSource)->setParent(this); +} + +PimEventsPlugin::PimEventsPlugin(PimDataSource *dataSource, QObject *parent) + : CalendarEvents::CalendarEventsPlugin(parent) + , mDataSource(dataSource) +{ + qCDebug(PIMEVENTSPLUGIN_LOG) << "PIM Events Plugin activated"; + + dataSource->calendar()->registerObserver(this); +} + + +PimEventsPlugin::~PimEventsPlugin() +{ + qCDebug(PIMEVENTSPLUGIN_LOG) << "PIM Events Plugin deactivated"; +} + + +void PimEventsPlugin::loadEventsForDateRange(const QDate &startDate, const QDate &endDate) +{ + mStart = startDate; + mEnd = endDate; + + int eventsCount = 0, eventDataCount = 0; + { + EventDataVisitor visitor(mDataSource, startDate, endDate); + const KCalCore::Event::List events = mDataSource->calendar()->events(startDate, endDate); + eventsCount = events.count(); + if (visitor.act(events)) { + eventDataCount = visitor.results().count(); + Q_EMIT dataReady(visitor.results()); + } + } + + int todosCount = 0, todoDataCount = 0; + { + EventDataVisitor visitor(mDataSource, startDate, endDate); + const KCalCore::Todo::List todos = mDataSource->calendar()->todos(startDate, endDate); + todosCount = todos.count(); + if (visitor.act(todos)) { + todoDataCount = visitor.results().count(); + Q_EMIT dataReady(visitor.results()); + } + } + qCDebug(PIMEVENTSPLUGIN_LOG) << "Range:" << startDate.toString(Qt::ISODate) << "-" << endDate.toString(Qt::ISODate) + << "Events:" << eventsCount + << "EventData:" << eventDataCount + << "Todos:" << todosCount + << "TodoData:" << todoDataCount; +} + +void PimEventsPlugin::calendarIncidenceAdded(const KCalCore::Incidence::Ptr &incidence) +{ + if (!mStart.isValid() || !mEnd.isValid()) { + // Don't bother with changes that happen before the applet starts populating data + return; + } + EventDataVisitor visitor(mDataSource, mStart, mEnd); + if (visitor.act(incidence)) { + Q_EMIT dataReady(visitor.results()); + } +} + +void PimEventsPlugin::calendarIncidenceChanged(const KCalCore::Incidence::Ptr &incidence) +{ + if (!mStart.isValid() || !mEnd.isValid()) { + return; + } + EventDataVisitor visitor(mDataSource, mStart, mEnd); + if (visitor.act(incidence)) { + Q_FOREACH (const auto &ed, visitor.results()) { + Q_EMIT eventModified(ed); + } + } +} + +void PimEventsPlugin::calendarIncidenceAboutToBeDeleted(const KCalCore::Incidence::Ptr &incidence) +{ + if (!mStart.isValid() || !mEnd.isValid()) { + return; + } + EventDataIdVisitor visitor(mDataSource, mStart, mEnd); + if (visitor.act(incidence)) { + Q_FOREACH (const QString &uid, visitor.results()) { + Q_EMIT eventRemoved(uid); + } + } +} diff --git a/plugins/plasma/pimeventsplugin/pimeventsplugin.h b/plugins/plasma/pimeventsplugin/pimeventsplugin.h new file mode 100644 index 0000000000000000000000000000000000000000..86ba4804d46d4c8e2ba8263edb54f1a0ea8de6b5 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/pimeventsplugin.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 PIMEVENTSPLUGIN_H +#define PIMEVENTSPLUGIN_H + +#include +#include + +namespace Akonadi +{ +class ChangeRecorder; +class ETMCalendar; +} + +class PimDataSource; +class PimEventsPlugin : public CalendarEvents::CalendarEventsPlugin + , public KCalCore::Calendar::CalendarObserver +{ + Q_OBJECT + Q_INTERFACES(CalendarEvents::CalendarEventsPlugin) + Q_PLUGIN_METADATA(IID "org.kde.CalendarEventsPlugin" FILE "pimeventsplugin.json") + +public: + explicit PimEventsPlugin(QObject *parent = Q_NULLPTR); + explicit PimEventsPlugin(PimDataSource *factory, QObject *parent = Q_NULLPTR); + ~PimEventsPlugin(); + + // CalendarEvents::CalendarEventsPlugin + void loadEventsForDateRange(const QDate &startDate, const QDate &endDate) Q_DECL_OVERRIDE; + + // KCalCore::Calendar::CalendarObserver + void calendarIncidenceChanged(const KCalCore::Incidence::Ptr &incidence) Q_DECL_OVERRIDE; + void calendarIncidenceAdded(const KCalCore::Incidence::Ptr & incidence) Q_DECL_OVERRIDE; + // Handle removal before it really happens otherwise we would not be able + // to lookup corresponding Akonadi ID in ETMCalendar + void calendarIncidenceAboutToBeDeleted(const KCalCore::Incidence::Ptr &incidence) Q_DECL_OVERRIDE; + +private: + PimDataSource *mDataSource; + QDate mStart; + QDate mEnd; +}; + +#endif diff --git a/plugins/plasma/pimeventsplugin/pimeventsplugin.json b/plugins/plasma/pimeventsplugin/pimeventsplugin.json new file mode 100644 index 0000000000000000000000000000000000000000..cae516ef1ace568a6598499f1491640516641cda --- /dev/null +++ b/plugins/plasma/pimeventsplugin/pimeventsplugin.json @@ -0,0 +1,10 @@ +{ + "Name": "PIM Events Plugin", + "Description": "Calendar plugin for displaying events from KDE PIM calendars", + "Icon": "view-calendar", + "ConfigUi": "pimevents/PimEventsConfig.qml", + "Authors": { + "Name": "Daniel Vrátil", + "Email": "dvratil@kde.org" + } +} diff --git a/plugins/plasma/pimeventsplugin/qmldir b/plugins/plasma/pimeventsplugin/qmldir new file mode 100644 index 0000000000000000000000000000000000000000..91ac0fe9cd25a174c446ca063a54e72887f5aec3 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/qmldir @@ -0,0 +1,3 @@ +module org.kde.plasma.PimCalendars + +plugin pimcalendarsplugin diff --git a/plugins/plasma/pimeventsplugin/settingschangenotifier.cpp b/plugins/plasma/pimeventsplugin/settingschangenotifier.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f8fcae15a7549bcda3e2372157af868d977b32c0 --- /dev/null +++ b/plugins/plasma/pimeventsplugin/settingschangenotifier.cpp @@ -0,0 +1,52 @@ +#include "settingschangenotifier.h" +#include "pimeventsplugin_debug.h" + +#include + +#define APP_PROPERTY_NAME "PIMEventsPluginSettingsChangeNotifier" + +SettingsChangeNotifier *SettingsChangeNotifier::self() +{ + // We can't easilly use a global static (or a static member) to store the + // global instance of SettingsChangeNotifier. We need the same instance to + // be accessible by both PimEventsPlugin and PimCalendarsPlugin so I would + // have to put this class to a .so and link it from both to get a + // singleton that actually works across the plugins. But being the lazy + // bastard that I am I decided to just abuse QObject::property() and the qApp + // singleton which already comes from an .so linked by both plugins. + // + // Also note the cast to quintptr: we have the same problem as above with + // SettingsChangeNotifier::staticMetaObject as each "copy" of the class + // has its own instance of it, which causes pointer comparision in + // QMetaObject::inherits() to fail. This leads to v.isValid() being true but + // v.value() returning a null pointer, because + // the internal qobject_cast fails. + // + // Yeah, I could have totally spent 30 seconds of my time and write the 6 + // lines of CMake code to get my own .so and have it linked from both plugins, + // but instead I decided to explain myself in this comment, probably because + // short code with long comments makes it look like I know what I'm doing. + const QVariant v = qApp->property(APP_PROPERTY_NAME); + if (v.isValid()) { + return reinterpret_cast(v.value()); + } + + SettingsChangeNotifier *notifier = new SettingsChangeNotifier(); + qApp->setProperty(APP_PROPERTY_NAME, reinterpret_cast(notifier)); + return notifier; +} + +SettingsChangeNotifier::SettingsChangeNotifier(QObject *parent) + : QObject(parent) +{ + qCDebug(PIMEVENTSPLUGIN_LOG) << this << "created"; +} + +SettingsChangeNotifier::~SettingsChangeNotifier() +{ +} + +void SettingsChangeNotifier::notifySettingsChanged() +{ + Q_EMIT settingsChanged(); +} diff --git a/plugins/plasma/pimeventsplugin/settingschangenotifier.h b/plugins/plasma/pimeventsplugin/settingschangenotifier.h new file mode 100644 index 0000000000000000000000000000000000000000..698e0ab3d3748ddc878a20f4d471a06b2659b29f --- /dev/null +++ b/plugins/plasma/pimeventsplugin/settingschangenotifier.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2016 Daniel Vrátil + * + * 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) any later version. + * + * 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 SETTINGSCHANGENOTIFIER_H +#define SETTINGSCHANGENOTIFIER_H + +#include + +class SettingsChangeNotifier : public QObject +{ + Q_OBJECT + +public: + static SettingsChangeNotifier *self(); + + ~SettingsChangeNotifier(); + + void notifySettingsChanged(); + +Q_SIGNALS: + void settingsChanged(); + +private: + explicit SettingsChangeNotifier(QObject *parent = Q_NULLPTR); +}; + +#endif