1 /*
2 * SPDX-FileCopyrightText: 2020 Dimitris Kardarakos <dimkard@posteo.net>
3 *
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7 #include "calendarcontroller.h"
8 #include "localcalendar.h"
9 #include <QStandardPaths>
10 #include <QDebug>
11 #include <QFile>
12 #include <QStringLiteral>
13 #include <KLocalizedString>
14 #include <KCalendarCore/ICalFormat>
15 #include <KCalendarCore/Calendar>
16 #include <KCalendarCore/MemoryCalendar>
17 #include <KCalendarCore/Attendee>
18 #include <KCalendarCore/Person>
19 #include "attendeesmodel.h"
20 #include "calindoriconfig.h"
21
CalendarController(QObject * parent)22 CalendarController::CalendarController(QObject *parent) : QObject {parent}, m_events {}, m_todos {}
23 {
24 }
25
importCalendarData(const QByteArray & data)26 void CalendarController::importCalendarData(const QByteArray &data)
27 {
28 KCalendarCore::MemoryCalendar::Ptr importedCalendar {new KCalendarCore::MemoryCalendar {QTimeZone::systemTimeZoneId()}};
29 KCalendarCore::ICalFormat icalFormat {};
30 auto readResult = icalFormat.fromRawString(importedCalendar, data);
31
32 if (!readResult) {
33 qDebug() << "The file read was not a valid calendar";
34 Q_EMIT statusMessageChanged(i18n("The url or file given does not contain valid calendar data"), MessageType::NegativeAnswer);
35 return;
36 }
37
38 m_events = importedCalendar->rawEvents();
39 m_todos = importedCalendar->rawTodos();
40
41 if (m_events.isEmpty() && m_todos.isEmpty()) {
42 qDebug() << "No events or tasks found.";
43 Q_EMIT statusMessageChanged(i18n("The url or file given does not contain any event or task"), MessageType::NegativeAnswer);
44 return;
45 }
46
47 QString confirmMsg {};
48
49 if (!m_events.isEmpty() && m_todos.isEmpty()) {
50 confirmMsg = i18np("1 event will be added", "%1 events will be added", m_events.count());
51 } else if (m_events.isEmpty() && !m_todos.isEmpty()) {
52 confirmMsg = i18np("1 task will be added", "%1 tasks will be added", m_todos.count());
53 } else {
54 auto incidenceCount = m_events.count() + m_todos.count();
55 confirmMsg = i18n("%1 incidences will be added", incidenceCount);
56 }
57
58 Q_EMIT statusMessageChanged(confirmMsg, MessageType::Question);
59 }
60
importFromBuffer(LocalCalendar * localCalendar)61 void CalendarController::importFromBuffer(LocalCalendar *localCalendar)
62 {
63 auto calendar = localCalendar->calendar();
64
65 for (const auto &event : qAsConst(m_events)) {
66 calendar->addEvent(event);
67 }
68
69 for (const auto &todo : qAsConst(m_todos)) {
70 calendar->addTodo(todo);
71 }
72
73 bool result = localCalendar->save();
74
75 if (!m_events.isEmpty()) {
76 Q_EMIT localCalendar->eventsChanged();
77 m_events.clear();
78 }
79
80 if (!m_todos.isEmpty()) {
81 Q_EMIT localCalendar->todosChanged();
82 m_todos.clear();
83 }
84
85 sendMessage(result);
86 }
87
importFromBuffer(const QString & targetCalendar)88 void CalendarController::importFromBuffer(const QString &targetCalendar)
89 {
90 auto filePath = CalindoriConfig {}.calendarFile(targetCalendar);
91 QFile calendarFile {filePath};
92 if (!calendarFile.exists()) {
93 sendMessage(false);
94 return;
95 }
96
97 Calendar::Ptr calendar {new MemoryCalendar(QTimeZone::systemTimeZoneId())};
98 FileStorage::Ptr storage {new FileStorage {calendar}};
99 storage->setFileName(filePath);
100 if (!storage->load()) {
101 sendMessage(false);
102 return;
103 }
104
105 for (const auto &event : qAsConst(m_events)) {
106 calendar->addEvent(event);
107 }
108
109 for (const auto &todo : qAsConst(m_todos)) {
110 calendar->addTodo(todo);
111 }
112
113 sendMessage(storage->save());
114 }
115
sendMessage(const bool positive)116 void CalendarController::sendMessage(const bool positive)
117 {
118 if (positive) {
119 Q_EMIT statusMessageChanged(i18n("Import completed successfully"), MessageType::PositiveAnswer);
120 } else {
121 Q_EMIT statusMessageChanged(i18n("An error has occurred during import"), MessageType::NegativeAnswer);
122 }
123 }
124
abortImporting()125 void CalendarController::abortImporting()
126 {
127 m_events.clear();
128 m_todos.clear();
129 }
130
removeEvent(LocalCalendar * localCalendar,const QVariantMap & eventData)131 void CalendarController::removeEvent(LocalCalendar *localCalendar, const QVariantMap &eventData)
132 {
133 Calendar::Ptr calendar = localCalendar->calendar();
134 QString uid = eventData["uid"].toString();
135 Event::Ptr event = calendar->event(uid);
136 calendar->deleteEvent(event);
137 bool deleted = localCalendar->save();
138 Q_EMIT localCalendar->eventsChanged();
139
140 qDebug() << "Event " << uid << " deleted: " << deleted;
141 }
142
upsertEvent(LocalCalendar * localCalendar,const QVariantMap & eventData,const QVariantList & attendeesList)143 void CalendarController::upsertEvent(LocalCalendar *localCalendar, const QVariantMap &eventData, const QVariantList &attendeesList)
144 {
145 qDebug() << "\naddEdit:\tCreating event";
146
147 Calendar::Ptr calendar = localCalendar->calendar();
148 QDateTime now = QDateTime::currentDateTime();
149 QString uid = eventData["uid"].toString();
150 QString summary = eventData["summary"].toString();
151 bool clearPartStatus {false};
152
153 QDate startDate = eventData["startDate"].toDate();
154 int startHour = eventData["startHour"].value<int>();
155 int startMinute = eventData["startMinute"].value<int>();
156
157 QDate endDate = eventData["endDate"].toDate();
158 int endHour = eventData["endHour"].value<int>();
159 int endMinute = eventData["endMinute"].value<int>();
160
161 QDateTime startDateTime;
162 QDateTime endDateTime;
163 bool allDayFlg = eventData["allDay"].toBool();
164
165 if (allDayFlg) {
166 startDateTime = startDate.startOfDay();
167 endDateTime = endDate.startOfDay();
168 } else {
169 startDateTime = QDateTime(startDate, QTime(startHour, startMinute, 0, 0), QTimeZone::systemTimeZone());
170 endDateTime = QDateTime(endDate, QTime(endHour, endMinute, 0, 0), QTimeZone::systemTimeZone());
171 }
172
173 Event::Ptr event;
174 if (uid == "") {
175 event = Event::Ptr(new Event());
176 event->setUid(summary.at(0) + now.toString("yyyyMMddhhmmsszzz"));
177 } else {
178 event = calendar->event(uid);
179 event->setUid(uid);
180 clearPartStatus = (event->dtStart() != startDateTime) || (event->dtEnd() != endDateTime) || (event->allDay() != allDayFlg);
181 }
182
183 event->setDtStart(startDateTime);
184 event->setDtEnd(endDateTime);
185 event->setDescription(eventData["description"].toString());
186 event->setSummary(summary);
187 event->setAllDay(allDayFlg);
188 event->setLocation(eventData["location"].toString());
189
190 event->clearAttendees();
191 for (auto &a : qAsConst(attendeesList)) {
192 auto attendee = a.value<KCalendarCore::Attendee>();
193 if (clearPartStatus) {
194 qDebug() << "Participants need to be informed";
195 attendee.setRSVP(true);
196 attendee.setStatus(KCalendarCore::Attendee::PartStat::NeedsAction);
197 }
198 event->addAttendee(attendee);
199 }
200
201 if (!attendeesList.isEmpty()) {
202 event->setOrganizer(KCalendarCore::Person { localCalendar->ownerName(), localCalendar->ownerEmail()});
203 }
204
205 event->clearAlarms();
206 QVariantList newAlarms = eventData["alarms"].value<QVariantList>();
207 QVariantList::const_iterator itr = newAlarms.constBegin();
208 while (itr != newAlarms.constEnd()) {
209 Alarm::Ptr newAlarm = event->newAlarm();
210 QHash<QString, QVariant> newAlarmHashMap = (*itr).value<QHash<QString, QVariant>>();
211 int startOffsetValue = newAlarmHashMap["startOffsetValue"].value<int>();
212 int startOffsetType = newAlarmHashMap["startOffsetType"].value<int>();
213 int actionType = newAlarmHashMap["actionType"].value<int>();
214
215 qDebug() << "addEdit:\tAdding alarm with start offset value " << startOffsetValue;
216 newAlarm->setStartOffset(Duration(startOffsetValue, static_cast<Duration::Type>(startOffsetType)));
217 newAlarm->setType(static_cast<Alarm::Type>(actionType));
218 newAlarm->setEnabled(true);
219 newAlarm->setText((event->summary()).isEmpty() ? event->description() : event->summary());
220 ++itr;
221 }
222
223 ushort newPeriod = static_cast<ushort>(eventData["periodType"].toInt());
224
225 //Bother with recurrences only if a recurrence has been found, either existing or new
226 if ((event->recurrenceType() != Recurrence::rNone) || (newPeriod != Recurrence::rNone)) {
227 //WORKAROUND: When changing an event from non-recurring to recurring, duplicate events are displayed
228 if (uid != "") {
229 calendar->deleteEvent(event);
230 }
231
232 switch (newPeriod) {
233 case Recurrence::rYearlyDay:
234 case Recurrence::rYearlyMonth:
235 case Recurrence::rYearlyPos:
236 event->recurrence()->setYearly(eventData["repeatEvery"].toInt());
237 break;
238 case Recurrence::rMonthlyDay:
239 case Recurrence::rMonthlyPos:
240 event->recurrence()->setMonthly(eventData["repeatEvery"].toInt());
241 break;
242 case Recurrence::rWeekly:
243 event->recurrence()->setWeekly(eventData["repeatEvery"].toInt());
244 break;
245 case Recurrence::rDaily:
246 event->recurrence()->setDaily(eventData["repeatEvery"].toInt());
247 break;
248 default:
249 event->recurrence()->clear();
250 }
251
252 if (newPeriod != Recurrence::rNone) {
253 int stopAfter = eventData["stopAfter"].toInt() > 0 ? eventData["stopAfter"].toInt() : -1;
254 event->recurrence()->setDuration(stopAfter);
255 event->recurrence()->setAllDay(allDayFlg);
256 }
257
258 if (uid != "") {
259 calendar->addEvent(event);
260 }
261 }
262
263 event->setStatus(eventData["status"].value<KCalendarCore::Incidence::Status>());
264
265 if (uid == "") {
266 calendar->addEvent(event);
267 }
268
269 bool merged = localCalendar->save();
270 Q_EMIT localCalendar->eventsChanged();
271
272 qDebug() << "Event upsert: " << merged;
273 }
274
localSystemDateTime() const275 QDateTime CalendarController::localSystemDateTime() const
276 {
277 return QDateTime::currentDateTime();
278 }
279
validateEvent(const QVariantMap & eventMap) const280 QVariantMap CalendarController::validateEvent(const QVariantMap &eventMap) const
281 {
282 QVariantMap result {};
283
284 QDate startDate = eventMap["startDate"].toDate();
285 bool validStartHour {false};
286 int startHour = eventMap["startHour"].toInt(&validStartHour);
287 bool validStartMinutes {false};
288 int startMinute = eventMap["startMinute"].toInt(&validStartMinutes);
289 QDate endDate = eventMap["endDate"].toDate();
290 bool validEndHour {false};
291 int endHour = eventMap["endHour"].toInt(&validEndHour);
292 bool validEndMinutes {false};
293 int endMinutes = eventMap["endMinute"].toInt(&validEndMinutes);
294 bool allDayFlg = eventMap["allDay"].toBool();
295
296 if (startDate.isValid() && validStartHour && validStartMinutes && endDate.isValid() && validEndHour && validEndMinutes) {
297 if (allDayFlg && (endDate != startDate)) {
298 result["success"] = false;
299 result["reason"] = i18n("In case of all day events, start date and end date should be equal");
300
301 return result;
302 }
303
304 auto startDateTime = QDateTime(startDate, QTime(startHour, startMinute, 0, 0), QTimeZone::systemTimeZone());
305 auto endDateTime = QDateTime(endDate, QTime(endHour, endMinutes, 0, 0), QTimeZone::systemTimeZone());
306
307 if (!allDayFlg && (startDateTime > endDateTime)) {
308 result["success"] = false;
309 result["reason"] = i18n("End date time should be equal to or greater than the start date time");
310 return result;
311 }
312
313 auto validPeriodType {false};
314 auto periodType = static_cast<ushort>(eventMap["periodType"].toInt(&validPeriodType));
315 auto eventDuration = startDateTime.secsTo(endDateTime);
316 auto validRepeatEvery {false};
317 auto repeatEvery = static_cast<ushort>(eventMap["repeatEvery"].toInt(&validRepeatEvery));
318
319 if (validPeriodType && (periodType == Recurrence::rDaily) && validRepeatEvery && (repeatEvery == 1) && eventDuration > 86400) {
320 result["success"] = false;
321 result["reason"] = i18n("Daily events should not span multiple days");
322 return result;
323 }
324 }
325
326 result["success"] = true;
327 result["reason"] = QString();
328
329 return result;
330
331 }
332
upsertTodo(LocalCalendar * localCalendar,const QVariantMap & todo)333 void CalendarController::upsertTodo(LocalCalendar *localCalendar, const QVariantMap &todo)
334 {
335 Calendar::Ptr calendar = localCalendar->calendar();
336 Todo::Ptr vtodo;
337 QDateTime now = QDateTime::currentDateTime();
338 QString uid = todo["uid"].toString();
339 QString summary = todo["summary"].toString();
340 QDate startDate = todo["startDate"].toDate();
341 int startHour = todo["startHour"].value<int>();
342 int startMinute = todo["startMinute"].value<int>();
343 bool allDayFlg = todo["allDay"].toBool();
344
345 if (uid == "") {
346 vtodo = Todo::Ptr(new Todo());
347 vtodo->setUid(summary.at(0) + now.toString("yyyyMMddhhmmsszzz"));
348 } else {
349 vtodo = calendar->todo(uid);
350 vtodo->setUid(uid);
351 }
352
353 QDateTime startDateTime;
354 if (allDayFlg) {
355 startDateTime = startDate.startOfDay();
356 } else {
357 startDateTime = QDateTime(startDate, QTime(startHour, startMinute, 0, 0), QTimeZone::systemTimeZone());
358 }
359
360 vtodo->setDtStart(startDateTime);
361
362 QDate dueDate = todo["dueDate"].toDate();
363
364 bool validDueHour {false};
365 int dueHour = todo["dueHour"].toInt(&validDueHour);
366
367 bool validDueMinutes {false};
368 int dueMinute = todo["dueMinute"].toInt(&validDueMinutes);
369
370 QDateTime dueDateTime = QDateTime();
371 if (dueDate.isValid() && validDueHour && validDueMinutes && !allDayFlg) {
372 dueDateTime = QDateTime(dueDate, QTime(dueHour, dueMinute, 0, 0), QTimeZone::systemTimeZone());
373 } else if (dueDate.isValid() && allDayFlg) {
374 dueDateTime = dueDate.startOfDay();
375 }
376
377 vtodo->setDtDue(dueDateTime);
378 vtodo->setDescription(todo["description"].toString());
379 vtodo->setSummary(summary);
380 vtodo->setAllDay((startDate.isValid() || dueDate.isValid()) ? allDayFlg : false);
381 vtodo->setLocation(todo["location"].toString());
382 vtodo->setCompleted(todo["completed"].toBool());
383
384 vtodo->clearAlarms();
385 QVariantList newAlarms = todo["alarms"].value<QVariantList>();
386 QVariantList::const_iterator itr = newAlarms.constBegin();
387 while (itr != newAlarms.constEnd()) {
388 Alarm::Ptr newAlarm = vtodo->newAlarm();
389 QHash<QString, QVariant> newAlarmHashMap = (*itr).value<QHash<QString, QVariant>>();
390 int startOffsetValue = newAlarmHashMap["startOffsetValue"].value<int>();
391 int startOffsetType = newAlarmHashMap["startOffsetType"].value<int>();
392 int actionType = newAlarmHashMap["actionType"].value<int>();
393
394 newAlarm->setStartOffset(Duration(startOffsetValue, static_cast<Duration::Type>(startOffsetType)));
395 newAlarm->setType(static_cast<Alarm::Type>(actionType));
396 newAlarm->setEnabled(true);
397 newAlarm->setText((vtodo->summary()).isEmpty() ? vtodo->description() : vtodo->summary());
398 ++itr;
399 }
400
401 calendar->addTodo(vtodo);
402 bool merged = localCalendar->save();
403
404 Q_EMIT localCalendar->todosChanged();
405
406 qDebug() << "Todo upsert: " << merged;
407 }
408
removeTodo(LocalCalendar * localCalendar,const QVariantMap & todo)409 void CalendarController::removeTodo(LocalCalendar *localCalendar, const QVariantMap &todo)
410 {
411 Calendar::Ptr calendar = localCalendar->calendar();
412 QString uid = todo["uid"].toString();
413 Todo::Ptr vtodo = calendar->todo(uid);
414
415 calendar->deleteTodo(vtodo);
416 bool removed = localCalendar->save();
417
418 Q_EMIT localCalendar->todosChanged();
419 qDebug() << "Todo deleted: " << removed;
420 }
421
validateTodo(const QVariantMap & todo) const422 QVariantMap CalendarController::validateTodo(const QVariantMap &todo) const
423 {
424 QVariantMap result {};
425
426 QDate startDate = todo["startDate"].toDate();
427 bool validStartHour {false};
428 int startHour = todo["startHour"].toInt(&validStartHour);
429 bool validStartMinutes {false};
430 int startMinute = todo["startMinute"].toInt(&validStartMinutes);
431 QDate dueDate = todo["dueDate"].toDate();
432 bool validDueHour {false};
433 int dueHour = todo["dueHour"].toInt(&validDueHour);
434 bool validDueMinutes {false};
435 int dueMinute = todo["dueMinute"].toInt(&validDueMinutes);
436 bool allDayFlg = todo["allDay"].toBool();
437
438 if (startDate.isValid() && validStartHour && validStartMinutes && dueDate.isValid() && validDueHour && validDueMinutes) {
439 if (allDayFlg && (dueDate != startDate)) {
440 result["success"] = false;
441 result["reason"] = i18n("In case of all day tasks, start date and due date should be equal");
442
443 return result;
444 }
445
446 if (!allDayFlg && (QDateTime(startDate, QTime(startHour, startMinute, 0, 0), QTimeZone::systemTimeZone()) > QDateTime(dueDate, QTime(dueHour, dueMinute, 0, 0), QTimeZone::systemTimeZone()))) {
447 result["success"] = false;
448 result["reason"] = i18n("Due date time should be equal to or greater than the start date time");
449 return result;
450 }
451 }
452
453 result["success"] = true;
454 result["reason"] = QString();
455
456 return result;
457 }
458
fileNameFromUrl(const QUrl & sourcePath)459 QString CalendarController::fileNameFromUrl(const QUrl &sourcePath)
460 {
461 return sourcePath.fileName();
462 }
463
exportData(const QString & calendarName)464 QVariantMap CalendarController::exportData(const QString &calendarName)
465 {
466 auto filePath = CalindoriConfig {}.calendarFile(calendarName);
467 QFile calendarFile {filePath};
468 if (!calendarFile.exists()) {
469 return {
470 { "success", false },
471 { "reason", i18n("Cannot read calendar. Export failed.") }
472 };
473 }
474
475 Calendar::Ptr calendar {new MemoryCalendar(QTimeZone::systemTimeZoneId())};
476 FileStorage::Ptr storage {new FileStorage {calendar}};
477 storage->setFileName(filePath);
478 if (!storage->load()) {
479 return {
480 { "success", false },
481 { "reason", i18n("Cannot load calendar. Export failed.") }
482 };
483 }
484
485 auto dirPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
486 QFile targetFile {dirPath + "/calindori_" + calendarName + ".ics"};
487 auto fileSuffix {1};
488 while (targetFile.exists()) {
489 targetFile.setFileName(dirPath + "/calindori_" + calendarName + "(" + QString::number(fileSuffix++) + ").ics");
490 }
491
492 storage->setFileName(targetFile.fileName());
493 if (!(storage->save())) {
494 return {
495 { "success", false },
496 { "reason", i18n("Cannot save calendar file. Export failed.") }
497 };
498
499 }
500
501 return {
502 { "success", true },
503 { "reason", i18n("Export completed successfully") },
504 { "targetFolder", QUrl {QStringLiteral("file://") + dirPath} }
505 };
506 }
507