MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 82.9% 943 / 0 / 1137
Functions: 86.0% 86 / 0 / 100
Branches: 64.5% 1433 / 0 / 2222

presentation/QuickPimBackend.cpp
Line Branch Exec Source
1 #include "data/TaskRecurrence.h"
2 #include "QuickPimBackend.h"
3 #include "QuickMailEventLink.h"
4 #include "TaskConsoleInput.h"
5 #include <algorithm>
6 #include <QLocale>
7 #include <QTimer>
8
9 #include "PimEditorTime.h"
10 #include "WorkspaceState.h"
11 #include "QuickPreferences.h"
12 #include "app/core/AccountSession.h"
13 #include "app/core/CalendarSyncCoordinator.h"
14 #include "app/core/ContactSyncCoordinator.h"
15 #include "app/core/MailSessionCore.h"
16 #include "data/CalendarStore.h"
17 #include "data/DavCredentials.h"
18 #include "data/ICalEventCodec.h"
19 #include "data/ICalendar.h"
20 #include "data/ICalTime.h"
21 #include "data/RecurrenceRule.h"
22 #include "data/CalendarSeries.h"
23 #include "presentation/CalendarEventDetails.h"
24 #include <QClipboard>
25 #include <QGuiApplication>
26 #include "data/MailCache.h"
27 #include "presentation/DavAccountSettings.h"
28 #include "service/NativeResourceDocument.h"
29 #include "ui/ThemeManager.h"
30 #include "util/MessageId.h"
31 #include <QColor>
32 #include <QQuickTextDocument>
33 #include <QSettings>
34 #include <QStandardPaths>
35 #include <QTimeZone>
36 #include <QUuid>
37 #include <QLoggingCategory>
38
39
4/7
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 8 taken 14 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
16 Q_LOGGING_CATEGORY(lcPimBackend, "mailjd.quick.pim")
40 #include <optional>
41
42 795 QuickPimBackend::QuickPimBackend(MailSessionCore &session, WorkspaceState &workspace,
43 795 QuickPreferences &preferences, Deps deps, QObject *parent)
44 795 : QObject(parent), m_session(session), m_workspace(workspace),
45 795 m_preferences(preferences), m_deps(std::move(deps)),
46 // The shared calendar database (same path as the Widgets frontend).
47
2/2
✓ Branch 9 → 10 taken 795 times.
✓ Branch 11 → 12 taken 795 times.
2385 m_calendar(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
48
1/1
✓ Branch 10 → 11 taken 795 times.
2385 QStringLiteral("/mailjd/cache/calendar.db"), this),
49
2/2
✓ Branch 19 → 20 taken 795 times.
✓ Branch 21 → 22 taken 795 times.
2385 m_tasks(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
50
1/1
✓ Branch 20 → 21 taken 795 times.
2385 QStringLiteral("/mailjd/cache/calendar.db"), this),
51 795 m_taskActions(session, workspace, m_tasks, {m_deps.acceptsIdentity, m_deps.accountName, m_deps.setStatus}, this),
52 // The shared cross-account contacts database (same path as Widgets).
53
2/2
✓ Branch 34 → 35 taken 795 times.
✓ Branch 36 → 37 taken 795 times.
2385 m_contacts(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
54
1/1
✓ Branch 35 → 36 taken 795 times.
2385 QStringLiteral("/mailjd/contacts.db"), this),
55
1/1
✓ Branch 42 → 43 taken 795 times.
10120 m_contactActions([&session] { return session.contactSync(); }, m_contacts, this),
56
1/1
✓ Branch 45 → 46 taken 795 times.
4017 m_contactNavigation([&session] { return session.contactSync(); }, this),
57
2/2
✓ Branch 60 → 61 taken 795 times.
✓ Branch 62 → 63 taken 795 times.
1590 m_nextUp(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
58
1/1
✓ Branch 61 → 62 taken 795 times.
2385 QStringLiteral("/mailjd/cache/calendar.db"),
59 2071 [this] { return m_calendar.hiddenCalendars(); }, this),
60
2/3
✓ Branch 3 → 4 taken 641 times.
✗ Branch 3 → 6 not taken.
✓ Branch 72 → 73 taken 795 times.
1436 m_contactMails([&session] { return session.cache() ? session.cache()->databasePath() : QString(); }, this),
61
1/1
✓ Branch 75 → 76 taken 795 times.
1996 m_contactLookup([&session] { return session.contactSync(); }, m_contacts, this),
62
3/3
✓ Branch 80 → 81 taken 795 times.
✓ Branch 81 → 82 taken 795 times.
✓ Branch 82 → 83 taken 795 times.
1590 m_contactMap(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/mailjd/contacts.db"),
63 this),
64
2/2
✓ Branch 90 → 91 taken 795 times.
✓ Branch 92 → 93 taken 795 times.
2385 m_contactGraph(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
65
1/1
✓ Branch 91 → 92 taken 795 times.
2385 QStringLiteral("/mailjd/contacts.db"),
66 this),
67
2/2
✓ Branch 100 → 101 taken 795 times.
✓ Branch 102 → 103 taken 795 times.
2385 m_contactMiniGraph(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
68
1/1
✓ Branch 101 → 102 taken 795 times.
2385 QStringLiteral("/mailjd/contacts.db"),
69
6/12
✓ Branch 68 → 69 taken 795 times.
✓ Branch 69 → 70 taken 795 times.
✓ Branch 70 → 71 taken 795 times.
✓ Branch 113 → 114 taken 795 times.
✓ Branch 115 → 116 taken 3975 times.
✓ Branch 115 → 117 taken 795 times.
✗ Branch 275 → 276 not taken.
✗ Branch 275 → 277 not taken.
✗ Branch 278 → 279 not taken.
✗ Branch 278 → 282 not taken.
✗ Branch 280 → 281 not taken.
✗ Branch 280 → 282 not taken.
8745 this) {
70 795 m_contactMiniGraph.setCompact(); // F103-40: the card's small graph shows relationships only
71 // Sprint 103 D-6: the account addresses suggest the "me" card.
72 795 m_contactActions.setOwnAddresses([&session] {
73 104 QStringList addresses;
74
4/4
✓ Branch 4 → 5 taken 104 times.
✓ Branch 5 → 6 taken 104 times.
✓ Branch 24 → 7 taken 104 times.
✓ Branch 24 → 25 taken 104 times.
208 for (const QVariant &account : session.accounts()->availableAccounts())
75
4/4
✓ Branch 8 → 9 taken 104 times.
✓ Branch 13 → 14 taken 104 times.
✓ Branch 14 → 15 taken 104 times.
✓ Branch 15 → 16 taken 104 times.
312 addresses << account.toMap().value(QStringLiteral("email")).toString();
76 104 return addresses;
77 ✗ });
78 // F103-06: the graph starts from me, also before the me card is chosen.
79 795 m_contactGraph.setMeFallback([this] {
80
1/1
✓ Branch 2 → 3 taken 57 times.
57 const QVariantList suggested = m_contactActions.meSuggestions();
81
15/25
✓ Branch 4 → 5 taken 38 times.
✓ Branch 4 → 6 taken 19 times.
✓ Branch 7 → 8 taken 19 times.
✓ Branch 12 → 13 taken 19 times.
✓ Branch 13 → 14 taken 19 times.
✓ Branch 15 → 16 taken 19 times.
✓ Branch 15 → 17 taken 38 times.
✓ Branch 17 → 18 taken 19 times.
✓ Branch 17 → 19 taken 38 times.
✓ Branch 19 → 20 taken 19 times.
✓ Branch 19 → 21 taken 38 times.
✓ Branch 21 → 22 taken 19 times.
✓ Branch 21 → 23 taken 38 times.
✓ Branch 23 → 24 taken 19 times.
✓ Branch 23 → 25 taken 38 times.
✗ Branch 28 → 29 not taken.
✗ Branch 28 → 30 not taken.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 38 not taken.
✗ Branch 40 → 41 not taken.
✗ Branch 40 → 42 not taken.
✗ Branch 44 → 45 not taken.
✗ Branch 44 → 46 not taken.
133 return suggested.isEmpty() ? QString() : suggested.first().toMap().value(QStringLiteral("key")).toString();
82 57 });
83
1/1
✓ Branch 127 → 128 taken 795 times.
2385 m_contactActions.setCalendarPath(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
84
1/1
✓ Branch 128 → 129 taken 795 times.
2385 QStringLiteral("/mailjd/cache/calendar.db"));
85 // S106-354: the Met sentence names the occasion the card refers to.
86 795 m_contacts.setEventTitleResolver([&session](const QString &uid) -> QString {
87 17 auto *sync = session.calendarSync();
88
3/6
✓ Branch 3 → 4 taken 17 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 17 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 17 times.
17 if (!sync || !sync->store()) return {};
89
4/5
✓ Branch 11 → 12 taken 17 times.
✓ Branch 12 → 13 taken 17 times.
✓ Branch 13 → 14 taken 17 times.
✗ Branch 32 → 15 not taken.
✓ Branch 32 → 33 taken 17 times.
17 for (const auto &[accountId, path] : sync->store()->calendarsWithUid(uid))
90
1/7
✗ Branch 19 → 20 not taken.
✗ Branch 21 → 22 not taken.
✗ Branch 21 → 25 not taken.
✗ Branch 27 → 28 not taken.
✗ Branch 27 → 30 not taken.
✓ Branch 35 → 36 taken 17 times.
✗ Branch 35 → 38 not taken.
17 if (const auto event = sync->store()->eventFor(uid, accountId, path)) return event->summary;
91 17 return {};
92 });
93
1/1
✓ Branch 140 → 141 taken 795 times.
2385 m_contactMails.setCalendarPath(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
94
1/1
✓ Branch 141 → 142 taken 795 times.
2385 QStringLiteral("/mailjd/cache/calendar.db"));
95 // Sprint 105 S105.5.2: index the addresses of caches older than schema
96 // v26 in the background, after the start-up work has settled.
97
1/1
✓ Branch 147 → 148 taken 795 times.
795 QTimer::singleShot(15000, &m_contactMails, &QuickContactMailHistory::startAddressBackfill);
98 // S103.5.8: relationship suggestions read the mail cache and the calendar (opt-in).
99
2/3
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 6 not taken.
✓ Branch 154 → 155 taken 795 times.
1592 m_contactGraph.setSuggestionSources([&session] { return session.cache() ? session.cache()->databasePath() : QString(); },
100
1/1
✓ Branch 151 → 152 taken 795 times.
1590 QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
101
1/1
✓ Branch 152 → 153 taken 795 times.
2385 QStringLiteral("/mailjd/cache/calendar.db"));
102 1590 }
103
104 // ─── D6: task write-back through the CalendarSyncCoordinator ───
105
106 // CalDAV/CardDAV sync projections and contact write-back.
107 // Wiring formerly QuickMailBackend::wirePim(); the backend calls this at the
108 // same position of its constructor (Sprint 91 rule 4).
109 795 void QuickPimBackend::connectSignals() {
110 795 MailSessionCore &session = m_session;
111 // D2: the session-owned CalDAV sync writes into the shared database; the
112 // projections reload on its storeChanged() signal so the month grid and
113 // task list stay fresh without user action.
114
2/2
✓ Branch 3 → 4 taken 794 times.
✓ Branch 3 → 11 taken 1 time.
795 if (session.calendarSync()) {
115
1/1
✓ Branch 5 → 6 taken 794 times.
794 connect(session.calendarSync(), &CalendarSyncCoordinator::storeChanged,
116 794 this, [this] {
117 186 m_calendar.reload();
118 186 m_tasks.reload();
119
1/4
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 7 taken 186 times.
✗ Branch 5 → 6 not taken.
✗ Branch 5 → 7 not taken.
186 if (m_agendaPanel && m_agendaPanelVisible) m_agendaPanel->reload();
120 186 m_nextUp.refresh();
121 186 ++m_taskLinkRevision;
122 186 emit taskLinksChanged();
123 186 emit calendarChoicesChanged();
124 186 });
125
1/1
✓ Branch 8 → 9 taken 794 times.
794 connect(session.calendarSync(), &CalendarSyncCoordinator::statusMessage,
126 1672 this, [this](const QString &message) { m_deps.setStatus(message); });
127 }
128
1/1
✓ Branch 11 → 12 taken 795 times.
795 connect(&m_taskActions, &QuickTaskActions::stateChanged, this, &QuickPimBackend::viewStateChanged);
129
1/1
✓ Branch 13 → 14 taken 795 times.
795 connect(&m_tasks, &QuickTaskModel::stateChanged, this, &QuickPimBackend::viewStateChanged);
130
2/5
✓ Branch 18 → 19 taken 795 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 795 times.
✗ Branch 112 → 113 not taken.
✗ Branch 112 → 114 not taken.
795 m_sync = new QuickCalendarSync(session.calendarSync(), session.contactSync(), this);
131
2/5
✓ Branch 24 → 25 taken 795 times.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 795 times.
✗ Branch 115 → 116 not taken.
✗ Branch 115 → 117 not taken.
795 m_push = new QuickDavPush(session.davPush(), session.calendarSync(), this);
132 // Period changes also notify; the host persists only when viewState()
133 // (view mode, work week, …) actually differs.
134
1/1
✓ Branch 27 → 28 taken 795 times.
795 connect(&m_calendar, &QuickCalendarModel::hiddenCalendarsChanged, this, &QuickPimBackend::viewStateChanged);
135
1/1
✓ Branch 29 → 30 taken 795 times.
795 connect(&m_calendar, &QuickCalendarModel::hiddenCalendarsChanged, &m_nextUp, &QuickNextUp::refresh);
136
1/1
✓ Branch 31 → 32 taken 795 times.
795 connect(&m_calendar, &QuickCalendarModel::hiddenCalendarsChanged, this, [this] {
137
1/3
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 7 taken 10 times.
✗ Branch 4 → 5 not taken.
10 if (m_agendaPanel) m_agendaPanel->setHiddenCalendars(m_calendar.hiddenCalendars());
138 10 });
139
1/1
✓ Branch 33 → 34 taken 795 times.
795 QTimer::singleShot(0, &m_nextUp, &QuickNextUp::refresh);
140
1/1
✓ Branch 34 → 35 taken 795 times.
795 connect(&m_calendar, &QuickCalendarModel::monthChanged, this, &QuickPimBackend::viewStateChanged);
141 // Q09: CardDAV contact sync keeps the paged contact projection fresh;
142 // routine completion is silent (the reload itself is the feedback).
143 795 m_calendar.setContactsDatabasePath(m_contacts.databasePath());
144
2/2
✓ Branch 40 → 41 taken 794 times.
✓ Branch 40 → 103 taken 1 time.
795 if (session.contactSync()) {
145
1/1
✓ Branch 41 → 42 taken 794 times.
794 connect(&m_preferences, &QuickPreferences::contactIntervalChanged, this, [this] {
146 21 m_session.contactSync()->stopBackgroundSync();
147 21 m_session.contactSync()->startBackgroundSync();
148 21 });
149
1/1
✓ Branch 44 → 45 taken 794 times.
794 connect(session.contactSync(), &ContactSyncCoordinator::contactsChanged,
150 794 this, [this] { m_contacts.reload(); });
151 // Sprint 97: per-card changes update rows in place (no reset, CB-08).
152
1/1
✓ Branch 47 → 48 taken 794 times.
794 connect(session.contactSync(), &ContactSyncCoordinator::cardsChanged,
153 794 &m_contacts, &QuickContactModel::applyCardChanges);
154
1/1
✓ Branch 50 → 51 taken 794 times.
794 connect(session.contactSync(), &ContactSyncCoordinator::booksChanged,
155 794 &m_contactActions, &QuickContactActions::booksChanged);
156
1/1
✓ Branch 53 → 54 taken 794 times.
794 connect(session.contactSync(), &ContactSyncCoordinator::statusMessage,
157 796 this, [this](const QString &message) { m_deps.setStatus(message); });
158
1/1
✓ Branch 56 → 57 taken 794 times.
794 connect(session.contactSync(), &ContactSyncCoordinator::syncStateChanged,
159 794 this, &QuickPimBackend::contactSyncStateChanged);
160
2/2
✓ Branch 64 → 60 taken 1588 times.
✓ Branch 64 → 65 taken 794 times.
2382 for (const auto signal : {&ContactSyncCoordinator::contactsChanged, &ContactSyncCoordinator::booksChanged})
161
1/1
✓ Branch 61 → 62 taken 1588 times.
1588 connect(session.contactSync(), signal, &m_contactNavigation, &QuickContactNavigation::noteChanged);
162
1/1
✓ Branch 66 → 67 taken 794 times.
794 connect(session.contactSync(), &ContactSyncCoordinator::cardsChanged,
163 794 &m_contactNavigation, &QuickContactNavigation::noteChanged);
164
1/1
✓ Branch 69 → 70 taken 794 times.
794 connect(session.contactSync(), &ContactSyncCoordinator::syncStateChanged,
165 794 &m_contactNavigation, &QuickContactNavigation::noteChanged);
166 794 connect(&m_contactActions, &QuickContactActions::booksChanged,
167
1/1
✓ Branch 71 → 72 taken 794 times.
794 &m_contactNavigation, &QuickContactNavigation::noteChanged);
168 // S97.5.10: the calendar's birthday layer follows contact changes.
169
1/1
✓ Branch 74 → 75 taken 794 times.
794 connect(session.contactSync(), &ContactSyncCoordinator::contactsChanged, this,
170
0/2
✗ Branch 3 → 4 not taken.
✗ Branch 3 → 5 not taken.
794 [this] { if (m_calendar.showBirthdays()) m_calendar.reload(); });
171
1/1
✓ Branch 77 → 78 taken 794 times.
794 connect(session.contactSync(), &ContactSyncCoordinator::cardsChanged, this,
172
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 42 times.
836 [this] { if (m_calendar.showBirthdays()) m_calendar.reload(); });
173
2/2
✓ Branch 85 → 81 taken 1588 times.
✓ Branch 85 → 86 taken 794 times.
2382 for (const auto signal : {&ContactSyncCoordinator::contactsChanged, &ContactSyncCoordinator::booksChanged})
174
1/1
✓ Branch 82 → 83 taken 1588 times.
1588 connect(session.contactSync(), signal, &m_contactLookup, &QuickContactLookup::invalidate);
175
1/1
✓ Branch 87 → 88 taken 794 times.
794 connect(session.contactSync(), &ContactSyncCoordinator::cardsChanged, &m_contactLookup, &QuickContactLookup::invalidate);
176
1/1
✓ Branch 90 → 91 taken 794 times.
794 connect(session.contactSync(), &ContactSyncCoordinator::cardsChanged, &m_contactMap, &QuickContactMap::reload);
177 // Families are one vCard each: older family data (member-of lines, families
178 // by name) is converted once at start and after a sync brought older cards.
179
1/1
✓ Branch 92 → 93 taken 794 times.
1517 QTimer::singleShot(0, &m_contactActions, [this] { m_contactActions.migrateFamilies(); });
180
1/1
✓ Branch 94 → 95 taken 794 times.
794 connect(session.contactSync(), &ContactSyncCoordinator::contactsChanged, &m_contactActions,
181 794 [this] { m_contactActions.migrateFamilies(); });
182
1/1
✓ Branch 97 → 98 taken 794 times.
794 connect(session.contactSync(), &ContactSyncCoordinator::cardsChanged, &m_contactGraph, &QuickContactGraph::reload);
183
1/1
✓ Branch 100 → 101 taken 794 times.
794 connect(session.contactSync(), &ContactSyncCoordinator::cardsChanged, &m_contactMiniGraph, &QuickContactGraph::reload);
184 }
185 795 }
186
187 57 QVariantMap QuickPimBackend::taskRecord(const QString &uid, const QString &accountId, const QString &calendarPath) const { return m_taskActions.taskRecord(uid, accountId, calendarPath); }
188
189 88 QVariantMap QuickPimBackend::taskAt(int row) const { return m_taskActions.taskAt(row); }
190
191 14 QVariantMap QuickPimBackend::createTaskFromInput(const QString &input) { return m_taskActions.createTaskFromInput(input); }
192
193 77 void QuickPimBackend::secureMarkdownPreview(QObject *wrapper) {
194 77 auto *target = qobject_cast<QQuickTextDocument *>(wrapper);
195
6/12
✓ Branch 3 → 4 taken 77 times.
✗ Branch 3 → 7 not taken.
✓ Branch 4 → 5 taken 77 times.
✓ Branch 5 → 6 taken 77 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 77 times.
✓ Branch 9 → 10 taken 77 times.
✗ Branch 9 → 11 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 77 times.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 27 not taken.
77 if (!target || target->property("mailjdResourceIsolated").toBool()) return;
196 // Reuse the mail reader's resource isolation. Task content can contain
197 // Markdown images, but opening notes must not read ambient files or URLs.
198
2/5
✓ Branch 15 → 16 taken 77 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 77 times.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
77 auto *document = new NativeResourceDocument(target);
199 77 target->setTextDocument(document);
200
1/1
✓ Branch 21 → 22 taken 77 times.
77 target->setProperty("mailjdResourceIsolated", true);
201 }
202
203 21 QVariantMap QuickPimBackend::saveTask(const QVariantMap &fields) { return m_taskActions.saveTask(fields); }
204
205 2567 QVariantMap QuickPimBackend::currentMailReference() const { return m_taskActions.currentMailReference(); }
206
207 2469 QVariantList QuickPimBackend::tasksForCurrentMail() const {
208
3/3
✓ Branch 2 → 3 taken 2469 times.
✓ Branch 7 → 8 taken 2469 times.
✓ Branch 8 → 9 taken 2469 times.
4938 const QString messageId = currentMailReference().value(QStringLiteral("messageId")).toString();
209
3/3
✓ Branch 15 → 16 taken 2133 times.
✓ Branch 15 → 17 taken 336 times.
✓ Branch 17 → 18 taken 336 times.
4938 return messageId.isEmpty() ? QVariantList{} : m_taskActions.tasksForMessage(messageId);
210 2469 }
211
212 981 QSet<QString> QuickPimBackend::openTaskMessageIds() const { return m_taskActions.openTaskMessageIds(); }
213
214 7 QVariantMap QuickPimBackend::taskMailReference(const QString &messageId) const { return m_taskActions.taskMailReference(messageId); }
215
216 5 bool QuickPimBackend::openTaskMail(const QString &messageId) { return m_taskActions.openTaskMail(messageId); }
217
218 ✗ bool QuickPimBackend::toggleTaskAt(int row) { return m_taskActions.toggleTaskAt(row); }
219
220 3 bool QuickPimBackend::toggleTask(const QString &uid, const QString &accountId, const QString &calendarPath) { return m_taskActions.toggleTask(uid, accountId, calendarPath); }
221
222 ✗ bool QuickPimBackend::removeTaskAt(int row) { return m_taskActions.removeTaskAt(row); }
223
224 1 bool QuickPimBackend::removeTask(const QString &uid, const QString &accountId,
225 1 const QString &calendarPath) { return m_taskActions.removeTask(uid, accountId, calendarPath); }
226
227 // ─── D5: event write-back through the CalendarSyncCoordinator ───
228
229 ✗ QVariantList QuickPimBackend::davCalendars() const {
230 ✗ QVariantList result;
231 ✗ if (const auto *sync = m_session.calendarSync())
232 ✗ for (const auto &calendar : sync->calendars())
233 ✗ result.append(QVariantMap{{QStringLiteral("path"), calendar.path},
234 ✗ {QStringLiteral("displayName"), calendar.displayName},
235 ✗ {QStringLiteral("accountId"), calendar.accountId}});
236 ✗ return result;
237 ✗ }
238
239 14942 QVariantList QuickPimBackend::calendarChoices() const {
240 14942 QVariantList result;
241 14942 const auto *sync = m_session.calendarSync();
242
3/6
✓ Branch 3 → 4 taken 14942 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 14942 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 14942 times.
14942 if (!sync || !sync->store()) return result;
243
1/1
✓ Branch 11 → 12 taken 14942 times.
14942 auto calendars = sync->store()->allCalendars();
244
3/3
✓ Branch 12 → 13 taken 14942 times.
✓ Branch 13 → 14 taken 14942 times.
✓ Branch 14 → 15 taken 14942 times.
14942 std::sort(calendars.begin(), calendars.end(), [](const auto &a, const auto &b) {
245 28481 const auto compared = a.displayName.localeAwareCompare(b.displayName);
246
8/12
✓ Branch 3 → 4 taken 27363 times.
✓ Branch 3 → 5 taken 1118 times.
✓ Branch 5 → 6 taken 1118 times.
✓ Branch 6 → 7 taken 1118 times.
✓ Branch 11 → 12 taken 1118 times.
✓ Branch 11 → 13 taken 27363 times.
✓ Branch 13 → 14 taken 1118 times.
✓ Branch 13 → 15 taken 27363 times.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 19 not taken.
✗ Branch 21 → 22 not taken.
✗ Branch 21 → 23 not taken.
28481 return compared ? compared < 0 : calendarSelectionKey(a.accountId, a.path) < calendarSelectionKey(b.accountId, b.path);
247 });
248 // Sidebar grouping (S93.3.2): "user @ host" names the DAV account.
249 14942 QHash<QString, QString> accountNames;
250
5/5
✓ Branch 15 → 16 taken 14942 times.
✓ Branch 16 → 17 taken 14942 times.
✓ Branch 17 → 18 taken 14942 times.
✓ Branch 37 → 19 taken 3622 times.
✓ Branch 37 → 38 taken 14942 times.
18564 for (const auto &account : DavAccountSettings::accounts())
251
5/5
✓ Branch 20 → 21 taken 3622 times.
✓ Branch 22 → 23 taken 3622 times.
✓ Branch 26 → 27 taken 3622 times.
✓ Branch 27 → 28 taken 3622 times.
✓ Branch 28 → 29 taken 3622 times.
25808 accountNames.insert(account.id, account.username + QStringLiteral(" @ ") + QUrl(account.serverUrl).host());
252
4/4
✓ Branch 39 → 40 taken 14942 times.
✓ Branch 40 → 41 taken 14942 times.
✓ Branch 79 → 42 taken 24648 times.
✓ Branch 79 → 80 taken 14942 times.
39590 for (const auto &calendar : calendars)
253
1/1
✓ Branch 68 → 69 taken 24648 times.
24648 result.append(QVariantMap{{"key", calendarSelectionKey(calendar.accountId, calendar.path)},
254
1/4
✗ Branch 46 → 47 not taken.
✓ Branch 46 → 48 taken 24648 times.
✗ Branch 47 → 53 not taken.
✗ Branch 51 → 53 not taken.
73944 {"accountName", accountNames.value(calendar.accountId, calendar.accountId.isEmpty() ? tr("On this device")
255
2/3
✓ Branch 49 → 50 taken 24648 times.
✗ Branch 50 → 51 not taken.
✓ Branch 50 → 52 taken 24648 times.
49296 : calendar.accountId.startsWith(QLatin1String("subscription:")) ? tr("Subscriptions") : calendar.accountId)},
256 49296 {"path", calendar.path}, {"accountId", calendar.accountId},
257 24648 {"displayName", calendar.displayName.isEmpty() ? calendar.path : calendar.displayName},
258
2/4
✓ Branch 72 → 73 taken 197184 times.
✓ Branch 72 → 74 taken 24648 times.
✗ Branch 111 → 112 not taken.
✗ Branch 111 → 113 not taken.
246480 {"color", calendar.color}, {"readOnly", calendar.readOnly}, {"supportsTasks", calendar.supportsTasks()}});
259 14942 return result;
260 64238 }
261
262 766 QStringList QuickPimBackend::calendarColorPalette() const { return ThemeManager::calendarPalette(); }
263
264 3 bool QuickPimBackend::setCalendarColor(const QString &accountId, const QString &path, const QString &color) {
265 3 const QColor parsed(color);
266 3 auto *sync = m_session.calendarSync();
267
7/10
✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 11 not taken.
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 11 not taken.
✓ Branch 8 → 9 taken 3 times.
✗ Branch 8 → 11 not taken.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 2 times.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 2 times.
3 if (!sync || !sync->store() || !parsed.isValid() || parsed.alpha() != 255) return false;
268
1/1
✓ Branch 16 → 17 taken 2 times.
2 const auto calendars = sync->store()->allCalendars();
269
3/3
✓ Branch 19 → 20 taken 2 times.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 22 taken 1 time.
2 if (std::none_of(calendars.begin(), calendars.end(), [&](const auto &calendar) {
270
3/4
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 7 taken 2 times.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 7 not taken.
3 return calendar.accountId == accountId && calendar.path == path;
271 1 })) return false;
272 // A local display preference is allowed for read-only DAV collections too.
273
2/2
✓ Branch 23 → 24 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
1 sync->store()->setCalendarColor(accountId, path, parsed.name());
274
3/4
✓ Branch 26 → 27 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 1 time.
1 if (sync->store()->calendarColor(accountId, path) != parsed.name()) return false;
275
2/2
✓ Branch 34 → 35 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
1 sync->pushCalendarColor(accountId, path, parsed.name()); // S93.5.8
276
1/1
✓ Branch 37 → 38 taken 1 time.
1 m_calendar.reload();
277
1/1
✓ Branch 38 → 39 taken 1 time.
1 m_tasks.reload();
278
1/1
✓ Branch 39 → 40 taken 1 time.
1 emit calendarChoicesChanged();
279 1 return true;
280 2 }
281
282 7 void QuickPimBackend::toggleCalendarVisibility(const QString &accountId, const QString &path) {
283
1/1
✓ Branch 2 → 3 taken 7 times.
7 const auto key = calendarSelectionKey(accountId, path);
284 7 auto hidden = m_calendar.hiddenCalendars();
285
3/3
✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 7 taken 4 times.
✓ Branch 6 → 8 taken 3 times.
7 if (hidden.contains(key)) hidden.removeAll(key);
286
1/1
✓ Branch 7 → 8 taken 4 times.
4 else hidden.append(key);
287 // F94-17: calendar visibility affects the calendar only; the task area
288 // has its own lists and always shows the tasks of every list.
289
1/1
✓ Branch 8 → 9 taken 7 times.
7 m_calendar.setHiddenCalendars(hidden);
290 7 }
291
292 1 void QuickPimBackend::setCalendarHourZoom(int zoom) {
293 1 zoom = qBound(32, zoom, 96);
294
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
1 if (zoom == m_hourZoom) return;
295 1 m_hourZoom = zoom;
296 1 emit viewStateChanged();
297 }
298
299 774 void QuickPimBackend::setAgendaPanelVisible(bool visible) {
300
2/2
✓ Branch 2 → 3 taken 765 times.
✓ Branch 2 → 4 taken 9 times.
774 if (visible == m_agendaPanelVisible) return;
301 9 m_agendaPanelVisible = visible;
302
4/4
✓ Branch 4 → 5 taken 7 times.
✓ Branch 4 → 24 taken 2 times.
✓ Branch 5 → 6 taken 6 times.
✓ Branch 5 → 24 taken 1 time.
9 if (visible && !m_agendaPanel) {
303
1/3
✓ Branch 10 → 11 taken 6 times.
✗ Branch 49 → 50 not taken.
✗ Branch 49 → 52 not taken.
12 m_agendaPanel = new QuickCalendarModel(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
304
3/4
✓ Branch 11 → 12 taken 6 times.
✓ Branch 12 → 13 taken 6 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 20 taken 6 times.
24 QStringLiteral("/mailjd/cache/calendar.db"), this);
305
1/1
✓ Branch 21 → 22 taken 6 times.
6 m_agendaPanel->setHiddenCalendars(m_calendar.hiddenCalendars());
306 }
307 // The panel always starts today.
308
5/5
✓ Branch 24 → 25 taken 7 times.
✓ Branch 24 → 35 taken 2 times.
✓ Branch 28 → 29 taken 7 times.
✓ Branch 29 → 30 taken 7 times.
✓ Branch 30 → 31 taken 7 times.
16 if (visible) m_agendaPanel->showDate(QDate::currentDate().toString(Qt::ISODate), QStringLiteral("agenda"));
309 9 emit viewStateChanged();
310 }
311
312 7 void QuickPimBackend::setCalendarWorkdayStart(int hour) {
313 7 hour = qBound(0, hour, 22);
314
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 6 times.
7 if (hour == m_workdayStart) return;
315 6 m_workdayStart = hour;
316
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 6 times.
6 if (m_workdayEnd <= hour) m_workdayEnd = hour + 1;
317 6 emit viewStateChanged();
318 }
319
320 7 void QuickPimBackend::setCalendarWorkdayEnd(int hour) {
321 7 hour = qBound(1, hour, 24);
322
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 6 times.
7 if (hour == m_workdayEnd) return;
323 6 m_workdayEnd = hour;
324
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 5 times.
6 if (m_workdayStart >= hour) m_workdayStart = hour - 1;
325 6 emit viewStateChanged();
326 }
327
328 775 void QuickPimBackend::setCalendarWorkdays(const QVariantList &days) {
329 775 QList<int> valid;
330
2/2
✓ Branch 16 → 4 taken 3883 times.
✓ Branch 16 → 17 taken 775 times.
4658 for (const QVariant &day : days)
331
8/10
✓ Branch 5 → 6 taken 3883 times.
✓ Branch 6 → 7 taken 3883 times.
✗ Branch 6 → 11 not taken.
✓ Branch 7 → 8 taken 3883 times.
✗ Branch 7 → 11 not taken.
✓ Branch 9 → 10 taken 3882 times.
✓ Branch 9 → 11 taken 1 time.
✓ Branch 12 → 13 taken 3882 times.
✓ Branch 12 → 14 taken 1 time.
✓ Branch 13 → 14 taken 3882 times.
3883 if (const int value = day.toInt(); value >= 1 && value <= 7 && !valid.contains(value)) valid.append(value);
332
2/2
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 774 times.
775 if (valid.isEmpty()) return; // at least one working day
333
3/3
✓ Branch 20 → 21 taken 774 times.
✓ Branch 21 → 22 taken 774 times.
✓ Branch 22 → 23 taken 774 times.
774 std::sort(valid.begin(), valid.end());
334 774 QVariantList list;
335
5/5
✓ Branch 23 → 24 taken 774 times.
✓ Branch 24 → 25 taken 774 times.
✓ Branch 28 → 29 taken 3882 times.
✓ Branch 32 → 26 taken 3882 times.
✓ Branch 32 → 33 taken 774 times.
4656 for (const int day : valid) list.append(day);
336
3/3
✓ Branch 33 → 34 taken 774 times.
✓ Branch 34 → 35 taken 766 times.
✓ Branch 34 → 36 taken 8 times.
774 if (list == m_workdays) return;
337 8 m_workdays = list;
338
1/1
✓ Branch 37 → 38 taken 8 times.
8 emit viewStateChanged();
339 1541 }
340
341 769 void QuickPimBackend::setCalendarFirstDayOfWeek(int day) {
342 769 day = qBound(0, day, 7);
343
2/2
✓ Branch 3 → 4 taken 765 times.
✓ Branch 3 → 5 taken 4 times.
769 if (day == m_firstDayOfWeek) return;
344 4 m_firstDayOfWeek = day;
345 // 0 follows the locale (QLocale::firstDayOfWeek, 1 = Monday … 7 = Sunday).
346
6/8
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 9 taken 3 times.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 3 times.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 20 not taken.
4 const int effective = day == 0 ? int(QLocale().firstDayOfWeek()) : day;
347 4 m_calendar.setFirstDayOfWeek(effective);
348 4 m_tasks.setWeekStart(effective);
349
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 4 times.
4 if (m_agendaPanel) m_agendaPanel->setFirstDayOfWeek(effective);
350 4 emit viewStateChanged();
351 }
352
353 641 bool QuickPimBackend::remindersEnabled() const {
354
3/3
✓ Branch 2 → 3 taken 641 times.
✓ Branch 8 → 9 taken 641 times.
✓ Branch 9 → 10 taken 641 times.
1282 return QSettings().value(QStringLiteral("calendar/remindersEnabled"), true).toBool();
355 }
356
357 2 void QuickPimBackend::setRemindersEnabled(bool enabled) {
358
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
2 if (enabled == remindersEnabled()) return;
359
2/2
✓ Branch 5 → 6 taken 2 times.
✓ Branch 11 → 12 taken 2 times.
4 QSettings().setValue(QStringLiteral("calendar/remindersEnabled"), enabled);
360 2 emit reminderSettingsChanged();
361 }
362
363 90 int QuickPimBackend::defaultReminder() const {
364
3/3
✓ Branch 2 → 3 taken 90 times.
✓ Branch 8 → 9 taken 90 times.
✓ Branch 9 → 10 taken 90 times.
180 return QSettings().value(QStringLiteral("calendar/defaultReminder"), 15).toInt();
365 }
366
367 2 void QuickPimBackend::setDefaultReminder(int minutes) {
368 2 minutes = qBound(-1, minutes, 7 * 1440);
369
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
2 if (minutes == defaultReminder()) return;
370
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 12 → 13 taken 2 times.
4 QSettings().setValue(QStringLiteral("calendar/defaultReminder"), minutes);
371 2 emit reminderSettingsChanged();
372 }
373
374 15 void QuickPimBackend::setLastCalendar(const QString &key) {
375
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 12 times.
15 if (key == m_lastCalendar) return;
376 12 m_lastCalendar = key;
377 12 emit viewStateChanged();
378 }
379
380 774 void QuickPimBackend::setAgendaTasksHeight(int height) {
381
2/2
✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 5 taken 766 times.
774 height = height <= 0 ? 0 : qBound(60, height, 2000);
382
2/2
✓ Branch 6 → 7 taken 766 times.
✓ Branch 6 → 8 taken 8 times.
774 if (height == m_agendaTasksHeight) return;
383 8 m_agendaTasksHeight = height;
384 8 emit viewStateChanged();
385 }
386
387 774 void QuickPimBackend::setAgendaPanelWidth(int width) {
388 774 width = qBound(240, width, 640);
389
2/2
✓ Branch 3 → 4 taken 765 times.
✓ Branch 3 → 5 taken 9 times.
774 if (width == m_agendaPanelWidth) return;
390 9 m_agendaPanelWidth = width;
391 9 emit viewStateChanged();
392 }
393
394 768 void QuickPimBackend::setAgendaTaskView(const QString &view) {
395
1/1
✓ Branch 2 → 3 taken 768 times.
768 const QString bounded = view.left(2100);
396
5/6
✓ Branch 4 → 5 taken 768 times.
✗ Branch 4 → 7 not taken.
✓ Branch 6 → 7 taken 765 times.
✓ Branch 6 → 8 taken 3 times.
✓ Branch 9 → 10 taken 765 times.
✓ Branch 9 → 11 taken 3 times.
768 if (bounded.isEmpty() || bounded == m_agendaTaskView) return;
397 3 m_agendaTaskView = bounded;
398
1/1
✓ Branch 12 → 13 taken 3 times.
3 emit viewStateChanged();
399 768 }
400
401 769 void QuickPimBackend::setAgendaTasksVisible(bool visible) {
402
2/2
✓ Branch 2 → 3 taken 767 times.
✓ Branch 2 → 4 taken 2 times.
769 if (visible == m_agendaTasksVisible) return;
403 2 m_agendaTasksVisible = visible;
404 2 emit viewStateChanged();
405 }
406
407 769 void QuickPimBackend::setAgendaTaskFilter(const QString &filter) {
408
2/2
✓ Branch 2 → 3 taken 769 times.
✓ Branch 3 → 4 taken 769 times.
769 const QString bounded = filter.left(256).simplified();
409
2/2
✓ Branch 6 → 7 taken 765 times.
✓ Branch 6 → 8 taken 4 times.
769 if (bounded == m_agendaTaskFilter) return;
410 4 m_agendaTaskFilter = bounded;
411
1/1
✓ Branch 9 → 10 taken 4 times.
4 emit viewStateChanged();
412 769 }
413
414 768 void QuickPimBackend::setAgendaTaskSort(const QString &sort) {
415
10/18
✓ Branch 8 → 9 taken 767 times.
✓ Branch 8 → 11 taken 1 time.
✓ Branch 10 → 11 taken 765 times.
✓ Branch 10 → 12 taken 2 times.
✓ Branch 13 → 14 taken 768 times.
✗ Branch 13 → 15 not taken.
✓ Branch 15 → 16 taken 768 times.
✗ Branch 15 → 19 not taken.
✓ Branch 17 → 18 taken 3072 times.
✓ Branch 17 → 19 taken 768 times.
✓ Branch 19 → 20 taken 766 times.
✓ Branch 19 → 21 taken 2 times.
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 26 not taken.
✗ Branch 28 → 29 not taken.
✗ Branch 28 → 32 not taken.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
3840 if (!QStringList{"due", "priority", "title", "created"}.contains(sort) || sort == m_agendaTaskSort) return;
416 2 m_agendaTaskSort = sort;
417 2 emit viewStateChanged();
418 768 }
419
420 314 QVariantMap QuickPimBackend::viewState() const {
421 628 return {{QStringLiteral("viewMode"), m_calendar.viewMode()},
422 628 {QStringLiteral("workWeek"), m_calendar.workWeek()},
423 628 {QStringLiteral("hourZoom"), m_hourZoom},
424 628 {QStringLiteral("hiddenCalendars"), m_calendar.hiddenCalendars()},
425 628 {QStringLiteral("lastCalendar"), m_lastCalendar},
426 628 {QStringLiteral("agendaPanel"), m_agendaPanelVisible},
427 628 {QStringLiteral("agendaWidth"), m_agendaPanelWidth},
428 628 {QStringLiteral("agendaTasksHeight"), m_agendaTasksHeight},
429 628 {QStringLiteral("agendaTasks"), m_agendaTaskView},
430 628 {QStringLiteral("agendaTasksShown"), m_agendaTasksVisible},
431 628 {QStringLiteral("agendaTaskFilter"), m_agendaTaskFilter},
432 628 {QStringLiteral("agendaTaskSort"), m_agendaTaskSort},
433 628 {QStringLiteral("workdayStart"), m_workdayStart},
434 628 {QStringLiteral("workdayEnd"), m_workdayEnd},
435 628 {QStringLiteral("workdays"), m_workdays},
436
2/4
✓ Branch 77 → 78 taken 5966 times.
✓ Branch 77 → 79 taken 314 times.
✗ Branch 118 → 119 not taken.
✗ Branch 118 → 120 not taken.
6908 {QStringLiteral("firstDayOfWeek"), m_firstDayOfWeek}, {"tasks", m_tasks.state()}, {"lastTaskList", m_taskActions.lastList()}, {"taskDefaultReminder", m_taskActions.defaultReminder()}};
437 5338 }
438
439 766 void QuickPimBackend::restoreViewState(const QVariantMap &state) {
440 // Values come normalized from UiStateStore; the setters ignore the rest.
441
2/2
✓ Branch 6 → 7 taken 766 times.
✓ Branch 7 → 8 taken 766 times.
1532 const QString mode = state.value(QStringLiteral("viewMode")).toString();
442
2/3
✓ Branch 13 → 14 taken 766 times.
✗ Branch 13 → 15 not taken.
✓ Branch 14 → 15 taken 766 times.
766 if (!mode.isEmpty()) m_calendar.setViewMode(mode);
443
3/3
✓ Branch 19 → 20 taken 766 times.
✓ Branch 20 → 21 taken 766 times.
✓ Branch 21 → 22 taken 766 times.
1532 m_calendar.setWorkWeek(state.value(QStringLiteral("workWeek")).toBool());
444
2/2
✓ Branch 30 → 31 taken 766 times.
✓ Branch 31 → 32 taken 766 times.
1532 const QStringList hidden = state.value(QStringLiteral("hiddenCalendars")).toStringList();
445
1/1
✓ Branch 36 → 37 taken 766 times.
766 m_calendar.setHiddenCalendars(hidden);
446
2/2
✓ Branch 41 → 42 taken 766 times.
✓ Branch 42 → 43 taken 766 times.
1532 const int zoom = state.value(QStringLiteral("hourZoom")).toInt();
447
1/2
✓ Branch 47 → 48 taken 766 times.
✗ Branch 47 → 50 not taken.
766 if (zoom > 0) m_hourZoom = qBound(32, zoom, 96);
448
2/2
✓ Branch 54 → 55 taken 766 times.
✓ Branch 55 → 56 taken 766 times.
1532 m_lastCalendar = state.value(QStringLiteral("lastCalendar")).toString();
449
2/3
✓ Branch 65 → 66 taken 766 times.
✓ Branch 68 → 69 taken 766 times.
✗ Branch 68 → 92 not taken.
766 if (state.contains(QStringLiteral("workdayStart"))) {
450
2/2
✓ Branch 73 → 74 taken 766 times.
✓ Branch 74 → 75 taken 766 times.
1532 m_workdayStart = qBound(0, state.value(QStringLiteral("workdayStart")).toInt(), 22);
451
2/2
✓ Branch 84 → 85 taken 766 times.
✓ Branch 85 → 86 taken 766 times.
1532 m_workdayEnd = qBound(m_workdayStart + 1, state.value(QStringLiteral("workdayEnd")).toInt(), 24);
452 }
453
3/3
✓ Branch 96 → 97 taken 766 times.
✓ Branch 97 → 98 taken 766 times.
✓ Branch 98 → 99 taken 766 times.
1532 setCalendarFirstDayOfWeek(state.value(QStringLiteral("firstDayOfWeek")).toInt());
454
5/6
✓ Branch 106 → 107 taken 766 times.
✓ Branch 109 → 110 taken 766 times.
✗ Branch 109 → 123 not taken.
✓ Branch 114 → 115 taken 766 times.
✓ Branch 115 → 116 taken 766 times.
✓ Branch 116 → 117 taken 766 times.
1532 if (state.contains(QStringLiteral("workdays"))) setCalendarWorkdays(state.value(QStringLiteral("workdays")).toList());
455
3/3
✓ Branch 127 → 128 taken 766 times.
✓ Branch 128 → 129 taken 766 times.
✓ Branch 129 → 130 taken 766 times.
1532 setAgendaPanelVisible(state.value(QStringLiteral("agendaPanel")).toBool());
456
5/6
✓ Branch 137 → 138 taken 766 times.
✓ Branch 140 → 141 taken 766 times.
✗ Branch 140 → 153 not taken.
✓ Branch 145 → 146 taken 766 times.
✓ Branch 146 → 147 taken 766 times.
✓ Branch 147 → 148 taken 766 times.
1532 if (state.contains(QStringLiteral("agendaWidth"))) setAgendaPanelWidth(state.value(QStringLiteral("agendaWidth")).toInt());
457
3/3
✓ Branch 157 → 158 taken 766 times.
✓ Branch 158 → 159 taken 766 times.
✓ Branch 159 → 160 taken 766 times.
1532 setAgendaTasksHeight(state.value(QStringLiteral("agendaTasksHeight")).toInt());
458
3/3
✓ Branch 168 → 169 taken 766 times.
✓ Branch 169 → 170 taken 766 times.
✓ Branch 170 → 171 taken 766 times.
1532 setAgendaTaskView(state.value(QStringLiteral("agendaTasks")).toString());
459
3/3
✓ Branch 180 → 181 taken 766 times.
✓ Branch 181 → 182 taken 766 times.
✓ Branch 182 → 183 taken 766 times.
1532 setAgendaTasksVisible(state.value(QStringLiteral("agendaTasksShown"), true).toBool());
460
3/3
✓ Branch 191 → 192 taken 766 times.
✓ Branch 192 → 193 taken 766 times.
✓ Branch 193 → 194 taken 766 times.
1532 setAgendaTaskFilter(state.value(QStringLiteral("agendaTaskFilter")).toString());
461
3/3
✓ Branch 206 → 207 taken 766 times.
✓ Branch 207 → 208 taken 766 times.
✓ Branch 208 → 209 taken 766 times.
1532 setAgendaTaskSort(state.value(QStringLiteral("agendaTaskSort"), QStringLiteral("due")).toString());
462
4/4
✓ Branch 217 → 218 taken 766 times.
✓ Branch 218 → 219 taken 766 times.
✓ Branch 219 → 220 taken 766 times.
✓ Branch 220 → 221 taken 766 times.
766 m_tasks.restoreState(state.value("tasks").toMap());
463
4/4
✓ Branch 226 → 227 taken 766 times.
✓ Branch 227 → 228 taken 766 times.
✓ Branch 228 → 229 taken 766 times.
✓ Branch 229 → 230 taken 766 times.
766 m_taskActions.setLastList(state.value("lastTaskList").toString());
464
4/4
✓ Branch 235 → 236 taken 766 times.
✓ Branch 236 → 237 taken 766 times.
✓ Branch 237 → 238 taken 766 times.
✓ Branch 238 → 239 taken 766 times.
766 m_taskActions.setDefaultReminder(state.value("taskDefaultReminder",0).toInt());
465
1/1
✓ Branch 242 → 243 taken 766 times.
766 emit viewStateChanged();
466 766 }
467
468 1 void QuickPimBackend::triggerContactSync() {
469
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 5 not taken.
1 if (const auto sync = m_session.contactSync()) sync->triggerSync();
470 1 }
471
472 1536 bool QuickPimBackend::contactSyncRunning() const {
473
3/4
✓ Branch 3 → 4 taken 1536 times.
✗ Branch 3 → 8 not taken.
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 8 taken 1534 times.
1536 return m_session.contactSync() && m_session.contactSync()->syncing();
474 }
475
476 namespace {
477 // Sprint 93 S93.1.7: the editor speaks inclusive all-day end dates; the
478 // store keeps the exclusive DTEND of RFC 5545. A missing end is one day.
479 58 QString editorEndText(const CalendarEvent &event) {
480
2/2
✓ Branch 2 → 3 taken 55 times.
✓ Branch 2 → 5 taken 3 times.
58 if (!event.allDay)
481
1/1
✓ Branch 3 → 4 taken 55 times.
55 return PimEditorTime::display(event.dtEnd, false);
482
2/2
✓ Branch 5 → 6 taken 3 times.
✓ Branch 6 → 7 taken 3 times.
3 const QDate start = event.dtStart.toUTC().date();
483
2/3
✓ Branch 8 → 9 taken 3 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 12 taken 3 times.
3 if (!event.dtEnd.isValid())
484 ✗ return start.toString(Qt::ISODate);
485
3/3
✓ Branch 12 → 13 taken 3 times.
✓ Branch 13 → 14 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
3 const QDate lastDay = event.dtEnd.toUTC().date().addDays(-1);
486
1/1
✓ Branch 17 → 18 taken 3 times.
3 return qMax(start, lastDay).toString(Qt::ISODate);
487 }
488
489 58 QVariantMap editorFields(const CalendarEvent &event, const QVariantMap &recurrence) {
490 116 return {{QStringLiteral("uid"), event.uid},
491 116 {QStringLiteral("accountId"), event.accountId},
492 116 {QStringLiteral("calendarPath"), event.calendarPath},
493 116 {QStringLiteral("summary"), event.summary},
494 116 {QStringLiteral("description"), event.description},
495 116 {QStringLiteral("location"), event.location},
496 116 {QStringLiteral("allDay"), event.allDay},
497 116 {QStringLiteral("rrule"), event.rrule},
498 116 {QStringLiteral("recurrence"), recurrence},
499
1/1
✓ Branch 45 → 46 taken 58 times.
174 {QStringLiteral("repeatCount"), recurrence.value(QStringLiteral("count"), 0)},
500
1/1
✓ Branch 54 → 55 taken 58 times.
174 {QStringLiteral("until"), recurrence.value(QStringLiteral("until"))},
501 116 {QStringLiteral("recurring"), !event.rrule.isEmpty()},
502
1/1
✓ Branch 64 → 65 taken 58 times.
116 {QStringLiteral("start"), PimEditorTime::display(event.dtStart, event.allDay)},
503
1/1
✓ Branch 69 → 70 taken 58 times.
116 {QStringLiteral("end"), editorEndText(event)},
504 116 {QStringLiteral("reminders"), [&event] {
505 58 QVariantList list;
506
3/3
✓ Branch 6 → 7 taken 10 times.
✓ Branch 10 → 4 taken 10 times.
✓ Branch 10 → 11 taken 58 times.
68 for (const int minutes : event.reminders) list.append(minutes);
507 58 return list;
508
3/5
✓ Branch 74 → 75 taken 58 times.
✓ Branch 78 → 79 taken 870 times.
✓ Branch 78 → 80 taken 58 times.
✗ Branch 124 → 125 not taken.
✗ Branch 124 → 126 not taken.
1044 }()}};
509 928 }
510 } // namespace
511
512 44 QVariantMap QuickPimBackend::eventAt(const QString &uid, const QString &accountId,
513 const QString &calendarPath) const {
514 44 const auto *sync = m_session.calendarSync();
515
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 44 times.
44 if (!sync)
516 ✗ return {};
517
1/1
✓ Branch 5 → 6 taken 44 times.
44 const auto stored = sync->eventFor(uid, accountId, calendarPath);
518
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 44 times.
44 if (!stored)
519 ✗ return {};
520 44 return editorFields(*stored, describeRecurrence(
521
5/6
✓ Branch 10 → 11 taken 44 times.
✓ Branch 11 → 12 taken 44 times.
✗ Branch 11 → 14 not taken.
✓ Branch 13 → 15 taken 44 times.
✓ Branch 16 → 17 taken 44 times.
✓ Branch 18 → 19 taken 44 times.
88 stored->rrule, stored->dtStart.isValid() ? stored->dtStart.toString(Qt::ISODate) : QString()));
522 44 }
523
524 321 QVariantMap QuickPimBackend::describeRecurrence(const QString &rrule, const QString &startIso) const {
525
3/5
✓ Branch 5 → 6 taken 321 times.
✓ Branch 9 → 10 taken 321 times.
✓ Branch 9 → 11 taken 321 times.
✗ Branch 341 → 342 not taken.
✗ Branch 341 → 343 not taken.
963 QVariantMap result{{QStringLiteral("rrule"), rrule.trimmed()}};
526
3/3
✓ Branch 14 → 15 taken 321 times.
✓ Branch 17 → 18 taken 267 times.
✓ Branch 17 → 47 taken 54 times.
321 if (rrule.trimmed().isEmpty()) {
527
1/1
✓ Branch 23 → 24 taken 267 times.
534 result.insert(QStringLiteral("frequency"), QString());
528
1/1
✓ Branch 32 → 33 taken 267 times.
534 result.insert(QStringLiteral("supported"), true);
529
2/2
✓ Branch 36 → 37 taken 267 times.
✓ Branch 41 → 42 taken 267 times.
534 result.insert(QStringLiteral("text"), tr("Does not repeat"));
530 267 return result;
531 }
532
1/1
✓ Branch 47 → 48 taken 54 times.
54 const RecurrenceRule rule = RecurrenceRule::parse(rrule);
533
1/1
✓ Branch 48 → 49 taken 54 times.
54 QDateTime start = QDateTime::fromString(startIso, Qt::ISODate);
534
2/3
✓ Branch 49 → 50 taken 54 times.
✗ Branch 50 → 51 not taken.
✓ Branch 50 → 55 taken 54 times.
54 if (!start.isValid())
535 ✗ start = QDateTime::currentDateTime();
536
1/1
✓ Branch 59 → 60 taken 54 times.
108 result.insert(QStringLiteral("valid"), rule.valid);
537
2/2
✓ Branch 63 → 64 taken 54 times.
✓ Branch 68 → 69 taken 54 times.
108 result.insert(QStringLiteral("frequency"), RecurrenceRule::frequencyName(rule.frequency));
538
1/1
✓ Branch 77 → 78 taken 54 times.
108 result.insert(QStringLiteral("interval"), rule.interval);
539 54 QVariantList weekdays;
540
2/2
✓ Branch 91 → 83 taken 13 times.
✓ Branch 91 → 92 taken 54 times.
67 for (const auto &entry : rule.byDay)
541
2/2
✓ Branch 84 → 85 taken 9 times.
✓ Branch 84 → 89 taken 4 times.
13 if (entry.ordinal == 0)
542
1/1
✓ Branch 86 → 87 taken 9 times.
9 weekdays.append(entry.weekday);
543
1/1
✓ Branch 96 → 97 taken 54 times.
108 result.insert(QStringLiteral("weekdays"), weekdays);
544 // Monthly mode the structured editor offers: a day of the month, the
545 // nth/last weekday, or the last day.
546 54 QString monthlyMode;
547
2/2
✓ Branch 100 → 101 taken 10 times.
✓ Branch 100 → 181 taken 44 times.
54 if (rule.frequency == RecurrenceRule::Frequency::Monthly) {
548
6/8
✓ Branch 102 → 103 taken 4 times.
✓ Branch 102 → 108 taken 6 times.
✓ Branch 104 → 105 taken 4 times.
✗ Branch 104 → 108 not taken.
✓ Branch 106 → 107 taken 4 times.
✗ Branch 106 → 108 not taken.
✓ Branch 109 → 110 taken 4 times.
✓ Branch 109 → 135 taken 6 times.
10 if (rule.byDay.size() == 1 && rule.byDay.first().ordinal != 0 && rule.byMonthDay.isEmpty()) {
549 4 monthlyMode = QStringLiteral("weekday");
550
1/1
✓ Branch 121 → 122 taken 4 times.
8 result.insert(QStringLiteral("ordinal"), rule.byDay.first().ordinal);
551
1/1
✓ Branch 130 → 131 taken 4 times.
8 result.insert(QStringLiteral("ordinalWeekday"), rule.byDay.first().weekday);
552
10/12
✓ Branch 136 → 137 taken 5 times.
✓ Branch 136 → 141 taken 1 time.
✓ Branch 137 → 138 taken 5 times.
✓ Branch 138 → 139 taken 5 times.
✓ Branch 139 → 140 taken 1 time.
✓ Branch 139 → 141 taken 4 times.
✓ Branch 142 → 143 taken 5 times.
✓ Branch 142 → 144 taken 1 time.
✓ Branch 144 → 145 taken 1 time.
✓ Branch 144 → 152 taken 5 times.
✗ Branch 455 → 456 not taken.
✗ Branch 455 → 457 not taken.
6 } else if (rule.byDay.isEmpty() && rule.byMonthDay == QList<int>{-1}) {
553 1 monthlyMode = QStringLiteral("lastDay");
554
5/6
✓ Branch 153 → 154 taken 4 times.
✓ Branch 153 → 157 taken 1 time.
✓ Branch 155 → 156 taken 4 times.
✗ Branch 155 → 157 not taken.
✓ Branch 158 → 159 taken 4 times.
✓ Branch 158 → 181 taken 1 time.
5 } else if (rule.byDay.isEmpty() && rule.byMonthDay.size() <= 1) {
555 4 monthlyMode = QStringLiteral("day");
556
2/3
✗ Branch 166 → 167 not taken.
✓ Branch 166 → 170 taken 4 times.
✓ Branch 176 → 177 taken 4 times.
12 result.insert(QStringLiteral("monthDay"),
557
0/2
✗ Branch 167 → 168 not taken.
✗ Branch 168 → 169 not taken.
12 rule.byMonthDay.isEmpty() ? start.date().day() : rule.byMonthDay.first());
558 }
559 }
560
1/1
✓ Branch 185 → 186 taken 54 times.
108 result.insert(QStringLiteral("monthlyMode"), monthlyMode);
561 54 QString endMode = QStringLiteral("never");
562
2/2
✓ Branch 193 → 194 taken 9 times.
✓ Branch 193 → 209 taken 45 times.
54 if (rule.count > 0) {
563 9 endMode = QStringLiteral("count");
564
1/1
✓ Branch 204 → 205 taken 9 times.
18 result.insert(QStringLiteral("count"), rule.count);
565
2/2
✓ Branch 210 → 211 taken 7 times.
✓ Branch 210 → 249 taken 38 times.
45 } else if (rule.hasUntil()) {
566 7 endMode = QStringLiteral("until");
567
2/2
✓ Branch 217 → 218 taken 7 times.
✓ Branch 218 → 219 taken 7 times.
7 const QDateTime until = rule.untilNaive(QTime(0, 0));
568
1/1
✓ Branch 219 → 220 taken 7 times.
7 QDate untilDate = until.date();
569
8/8
✓ Branch 220 → 221 taken 7 times.
✓ Branch 221 → 222 taken 6 times.
✓ Branch 221 → 225 taken 1 time.
✓ Branch 222 → 223 taken 6 times.
✓ Branch 223 → 224 taken 4 times.
✓ Branch 223 → 225 taken 2 times.
✓ Branch 226 → 227 taken 4 times.
✓ Branch 226 → 237 taken 3 times.
7 if (rule.untilIsUtc() && start.timeSpec() != Qt::UTC)
570
6/6
✓ Branch 227 → 228 taken 4 times.
✓ Branch 228 → 229 taken 4 times.
✓ Branch 229 → 230 taken 4 times.
✓ Branch 230 → 231 taken 4 times.
✓ Branch 231 → 232 taken 4 times.
✓ Branch 232 → 233 taken 4 times.
4 untilDate = QDateTime(until.date(), until.time(), QTimeZone::utc()).toLocalTime().date();
571
2/2
✓ Branch 237 → 238 taken 7 times.
✓ Branch 242 → 243 taken 7 times.
14 result.insert(QStringLiteral("until"), untilDate.toString(Qt::ISODate));
572 7 }
573
1/1
✓ Branch 253 → 254 taken 54 times.
108 result.insert(QStringLiteral("endMode"), endMode);
574 // The structured editor covers frequency + interval, weekly weekdays,
575 // the monthly modes above and one end; anything else is kept verbatim.
576
1/1
✓ Branch 259 → 260 taken 54 times.
54 const bool plainDays = std::all_of(rule.byDay.cbegin(), rule.byDay.cend(),
577 13 [](const auto &entry) { return entry.ordinal == 0; });
578
5/6
✓ Branch 262 → 263 taken 53 times.
✓ Branch 262 → 278 taken 1 time.
✓ Branch 264 → 265 taken 52 times.
✓ Branch 264 → 278 taken 1 time.
✓ Branch 266 → 267 taken 52 times.
✗ Branch 266 → 278 not taken.
106 bool supported = rule.valid && rule.unknownParts.isEmpty() && rule.bySetPos.isEmpty() &&
579
3/6
✓ Branch 268 → 269 taken 52 times.
✗ Branch 268 → 278 not taken.
✓ Branch 270 → 271 taken 52 times.
✗ Branch 270 → 278 not taken.
✓ Branch 272 → 273 taken 52 times.
✗ Branch 272 → 278 not taken.
156 rule.byMonth.isEmpty() && rule.byYearDay.isEmpty() && rule.byWeekNo.isEmpty() &&
580
3/6
✓ Branch 260 → 261 taken 54 times.
✗ Branch 260 → 278 not taken.
✓ Branch 274 → 275 taken 52 times.
✗ Branch 274 → 278 not taken.
✓ Branch 276 → 277 taken 52 times.
✗ Branch 276 → 278 not taken.
160 rule.byHour.isEmpty() && rule.byMinute.isEmpty() && rule.bySecond.isEmpty();
581
5/5
✓ Branch 279 → 280 taken 19 times.
✓ Branch 279 → 288 taken 22 times.
✓ Branch 279 → 295 taken 10 times.
✓ Branch 279 → 301 taken 1 time.
✓ Branch 279 → 309 taken 2 times.
54 switch (rule.frequency) {
582 19 case RecurrenceRule::Frequency::Daily:
583
4/6
✓ Branch 280 → 281 taken 18 times.
✓ Branch 280 → 286 taken 1 time.
✓ Branch 282 → 283 taken 18 times.
✗ Branch 282 → 286 not taken.
✓ Branch 284 → 285 taken 18 times.
✗ Branch 284 → 286 not taken.
19 supported = supported && rule.byDay.isEmpty() && rule.byMonthDay.isEmpty();
584 19 break;
585 22 case RecurrenceRule::Frequency::Weekly:
586
3/6
✓ Branch 288 → 289 taken 22 times.
✗ Branch 288 → 293 not taken.
✓ Branch 289 → 290 taken 22 times.
✗ Branch 289 → 293 not taken.
✓ Branch 291 → 292 taken 22 times.
✗ Branch 291 → 293 not taken.
22 supported = supported && plainDays && rule.byMonthDay.isEmpty();
587 22 break;
588 10 case RecurrenceRule::Frequency::Monthly:
589
3/4
✓ Branch 295 → 296 taken 9 times.
✓ Branch 295 → 299 taken 1 time.
✓ Branch 297 → 298 taken 9 times.
✗ Branch 297 → 299 not taken.
10 supported = supported && !monthlyMode.isEmpty();
590 10 break;
591 1 case RecurrenceRule::Frequency::Yearly:
592
3/6
✓ Branch 301 → 302 taken 1 time.
✗ Branch 301 → 307 not taken.
✓ Branch 303 → 304 taken 1 time.
✗ Branch 303 → 307 not taken.
✓ Branch 305 → 306 taken 1 time.
✗ Branch 305 → 307 not taken.
1 supported = supported && rule.byDay.isEmpty() && rule.byMonthDay.isEmpty();
593 1 break;
594 2 default:
595 2 supported = false;
596 }
597
1/1
✓ Branch 314 → 315 taken 54 times.
108 result.insert(QStringLiteral("supported"), supported);
598
1/1
✓ Branch 318 → 319 taken 54 times.
54 const QString text = ::describeRecurrence(rule, start);
599
2/4
✗ Branch 320 → 321 not taken.
✓ Branch 320 → 322 taken 54 times.
✗ Branch 321 → 323 not taken.
✓ Branch 327 → 328 taken 54 times.
108 result.insert(QStringLiteral("text"), text.isEmpty() ? tr("Unreadable recurrence rule") : text);
600 54 return result;
601 696 }
602
603 namespace {
604 // A stored zoned value stays in its zone when the editor returns the same
605 // instant; a changed instant is kept in the system zone (new events and
606 // moves are written with TZID = system zone, S93.1.2).
607 80 QDateTime sameInstantKeepZone(const QDateTime &stored, const QDateTime &edited) {
608
14/18
✓ Branch 2 → 3 taken 80 times.
✓ Branch 3 → 4 taken 62 times.
✓ Branch 3 → 9 taken 18 times.
✓ Branch 4 → 5 taken 62 times.
✓ Branch 5 → 6 taken 62 times.
✓ Branch 6 → 7 taken 62 times.
✓ Branch 7 → 8 taken 36 times.
✓ Branch 7 → 9 taken 26 times.
✓ Branch 10 → 11 taken 62 times.
✓ Branch 10 → 12 taken 18 times.
✓ Branch 12 → 13 taken 62 times.
✓ Branch 12 → 14 taken 18 times.
✓ Branch 14 → 15 taken 36 times.
✓ Branch 14 → 16 taken 44 times.
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 24 not taken.
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 28 not taken.
80 if (stored.isValid() && stored.toUTC() == edited.toUTC())
609 36 return stored;
610
2/2
✓ Branch 16 → 17 taken 44 times.
✓ Branch 17 → 18 taken 44 times.
44 return edited.toTimeZone(QTimeZone::systemTimeZone());
611 }
612 } // namespace
613
614 // saveEvent step: validate the Q11 recurrence fields and build the stored
615 // RRULE (empty = no recurrence). Returns the user-facing error or "".
616 47 QString QuickPimBackend::eventRecurrenceRule(const QVariantMap &fields,
617 QString &storedRule) const {
618 // Q11 recurrence editing: the editor offers the base frequencies plus an
619 // optional repetition end (COUNT). The backend accepts exactly that
620 // (normalized, case-insensitive, with or without the RFC 5545 FREQ=
621 // prefix) and stores the canonical RRULE the expansion engine supports.
622 // An edited series keeps its stored exdates.
623 ✗ static const QStringList kAllowedRules = {QStringLiteral("DAILY"),
624
5/10
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 28 taken 43 times.
✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 28 not taken.
✓ Branch 21 → 22 taken 16 times.
✓ Branch 21 → 23 taken 4 times.
✗ Branch 185 → 186 not taken.
✗ Branch 185 → 187 not taken.
✗ Branch 206 → 207 not taken.
✗ Branch 206 → 208 not taken.
67 QStringLiteral("WEEKLY"), QStringLiteral("MONTHLY"), QStringLiteral("YEARLY")};
625 // Sprint 93 S93.1.5 (B-02): a complete rule (the editor's prefill when the
626 // user did not touch the recurrence, or the structured editor's result)
627 // is stored exactly as given once it parses.
628
3/3
✓ Branch 32 → 33 taken 47 times.
✓ Branch 33 → 34 taken 47 times.
✓ Branch 34 → 35 taken 47 times.
94 const QString fullRule = fields.value(QStringLiteral("rrule")).toString().trimmed();
629
3/3
✓ Branch 42 → 43 taken 47 times.
✓ Branch 43 → 44 taken 12 times.
✓ Branch 43 → 52 taken 35 times.
47 if (fullRule.contains(QLatin1Char('='))) {
630
1/1
✓ Branch 44 → 45 taken 12 times.
12 const RecurrenceRule parsed = RecurrenceRule::parse(fullRule);
631
2/2
✓ Branch 45 → 46 taken 1 time.
✓ Branch 45 → 48 taken 11 times.
12 if (!parsed.valid)
632
1/1
✓ Branch 46 → 47 taken 1 time.
1 return tr("This recurrence rule cannot be read; choose a new one.");
633 11 storedRule = fullRule;
634 11 return {};
635 12 }
636
1/1
✓ Branch 52 → 53 taken 35 times.
35 QString rrule = fullRule.toUpper();
637
2/3
✓ Branch 56 → 57 taken 35 times.
✗ Branch 59 → 60 not taken.
✓ Branch 59 → 70 taken 35 times.
35 if (rrule.startsWith(QStringLiteral("FREQ=")))
638 ✗ rrule = rrule.mid(QStringLiteral("FREQ=").size());
639
6/6
✓ Branch 71 → 72 taken 2 times.
✓ Branch 71 → 75 taken 33 times.
✓ Branch 73 → 74 taken 1 time.
✓ Branch 73 → 75 taken 1 time.
✓ Branch 76 → 77 taken 1 time.
✓ Branch 76 → 79 taken 34 times.
35 if (!rrule.isEmpty() && !kAllowedRules.contains(rrule)) {
640
1/1
✓ Branch 77 → 78 taken 1 time.
1 return tr("Unsupported recurrence rule; choose daily, weekly, monthly, yearly or none.");
641 }
642 // Optional bounded end: a whole count between 1 and 999 (0/empty = open)
643 // OR an until date (ISO yyyy-MM-dd) — the two are mutually exclusive.
644 34 bool countOk = false;
645 const double countValue =
646
2/2
✓ Branch 83 → 84 taken 34 times.
✓ Branch 84 → 85 taken 34 times.
68 fields.value(QStringLiteral("count"), 0).toDouble(&countOk);
647 const QString untilText =
648
4/4
✓ Branch 93 → 94 taken 34 times.
✓ Branch 94 → 95 taken 34 times.
✓ Branch 95 → 96 taken 34 times.
✓ Branch 96 → 97 taken 34 times.
68 fields.value(QStringLiteral("until")).toString().trimmed().left(10);
649
1/1
✓ Branch 103 → 104 taken 34 times.
34 const QDate untilDate = QDate::fromString(untilText, Qt::ISODate);
650
9/11
✓ Branch 105 → 106 taken 33 times.
✓ Branch 105 → 111 taken 1 time.
✓ Branch 106 → 107 taken 33 times.
✗ Branch 106 → 110 not taken.
✓ Branch 107 → 108 taken 32 times.
✓ Branch 107 → 110 taken 1 time.
✓ Branch 108 → 109 taken 32 times.
✗ Branch 109 → 110 not taken.
✓ Branch 109 → 111 taken 32 times.
✓ Branch 112 → 113 taken 1 time.
✓ Branch 112 → 115 taken 33 times.
34 if (rrule.isEmpty() && !((countOk && countValue == 0) && !untilDate.isValid())) {
651
1/1
✓ Branch 113 → 114 taken 1 time.
1 return tr("A repetition end needs a recurrence; choose a frequency first.");
652 }
653
3/6
✓ Branch 115 → 116 taken 33 times.
✗ Branch 115 → 119 not taken.
✓ Branch 116 → 117 taken 33 times.
✗ Branch 116 → 119 not taken.
✓ Branch 117 → 118 taken 33 times.
✗ Branch 117 → 119 not taken.
33 if (!countOk || countValue < 0 || countValue > 999 ||
654
1/2
✗ Branch 118 → 119 not taken.
✓ Branch 118 → 121 taken 33 times.
33 std::floor(countValue) != countValue) {
655 ✗ return tr("Enter the repetition end as a whole number between 1 and 999.");
656 }
657
6/7
✓ Branch 121 → 122 taken 1 time.
✓ Branch 121 → 125 taken 32 times.
✓ Branch 122 → 123 taken 1 time.
✓ Branch 123 → 124 taken 1 time.
✗ Branch 123 → 125 not taken.
✓ Branch 126 → 127 taken 1 time.
✓ Branch 126 → 129 taken 32 times.
33 if (countValue > 0 && untilDate.isValid()) {
658
1/1
✓ Branch 127 → 128 taken 1 time.
1 return tr("Use either a repetition count or an end date, not both.");
659 }
660
2/7
✗ Branch 130 → 131 not taken.
✓ Branch 130 → 134 taken 32 times.
✗ Branch 131 → 132 not taken.
✗ Branch 132 → 133 not taken.
✗ Branch 132 → 134 not taken.
✗ Branch 135 → 136 not taken.
✓ Branch 135 → 138 taken 32 times.
32 if (!untilText.isEmpty() && !untilDate.isValid()) {
661 ✗ return tr("Enter a valid end date.");
662 }
663 32 const int repeatCount = static_cast<int>(countValue);
664 32 storedRule.clear();
665
1/2
✗ Branch 140 → 141 not taken.
✓ Branch 140 → 178 taken 32 times.
32 if (!rrule.isEmpty()) {
666 ✗ storedRule = QStringLiteral("FREQ=") + rrule;
667 ✗ if (repeatCount > 0)
668 ✗ storedRule += QStringLiteral(";COUNT=%1").arg(repeatCount);
669 ✗ else if (untilDate.isValid())
670 ✗ storedRule += QStringLiteral(";UNTIL=") + untilDate.toString(QStringLiteral("yyyyMMdd"));
671 }
672 32 return {};
673 67 }
674
675 // saveEvent step: the event record to write — the stored event, the
676 // occurrence exception of an edited single occurrence, or a new event on
677 // the requested calendar. Returns the user-facing error or "".
678 41 QString QuickPimBackend::eventForSave(CalendarSyncCoordinator *sync,
679 const QVariantMap &fields,
680 CalendarEvent &event, bool &wasNew) const {
681
2/2
✓ Branch 6 → 7 taken 41 times.
✓ Branch 7 → 8 taken 41 times.
82 const QString uid = fields.value(QStringLiteral("uid")).toString();
682
2/2
✓ Branch 16 → 17 taken 41 times.
✓ Branch 17 → 18 taken 41 times.
82 const QString accountId = fields.value(QStringLiteral("accountId")).toString();
683
2/2
✓ Branch 26 → 27 taken 41 times.
✓ Branch 27 → 28 taken 41 times.
82 const QString calendarPath = fields.value(QStringLiteral("calendarPath")).toString();
684 // Q11 occurrence scope: "this" edits the single occurrence anchored at
685 // occurrenceStart by creating or updating a RECURRENCE-ID exception.
686
2/2
✓ Branch 36 → 37 taken 41 times.
✓ Branch 37 → 38 taken 41 times.
82 const QString editScope = fields.value(QStringLiteral("editScope")).toString();
687 const QDateTime occurrenceStart = QDateTime::fromString(
688
3/3
✓ Branch 46 → 47 taken 41 times.
✓ Branch 47 → 48 taken 41 times.
✓ Branch 48 → 49 taken 41 times.
82 fields.value(QStringLiteral("occurrenceStart")).toString(), Qt::ISODate);
689 const bool editThisOccurrence =
690
6/13
✓ Branch 58 → 59 taken 3 times.
✓ Branch 58 → 62 taken 38 times.
✓ Branch 59 → 60 taken 3 times.
✓ Branch 60 → 61 taken 3 times.
✗ Branch 60 → 62 not taken.
✓ Branch 63 → 64 taken 41 times.
✗ Branch 63 → 65 not taken.
✓ Branch 65 → 66 taken 41 times.
✗ Branch 65 → 67 not taken.
✗ Branch 243 → 244 not taken.
✗ Branch 243 → 245 not taken.
✗ Branch 247 → 248 not taken.
✗ Branch 247 → 249 not taken.
82 editScope == QStringLiteral("this") && occurrenceStart.isValid();
691
7/15
✓ Branch 71 → 72 taken 3 times.
✓ Branch 71 → 75 taken 38 times.
✓ Branch 72 → 73 taken 3 times.
✗ Branch 73 → 74 not taken.
✓ Branch 73 → 75 taken 3 times.
✓ Branch 76 → 77 taken 41 times.
✗ Branch 76 → 78 not taken.
✓ Branch 78 → 79 taken 41 times.
✗ Branch 78 → 80 not taken.
✗ Branch 80 → 81 not taken.
✓ Branch 80 → 83 taken 41 times.
✗ Branch 251 → 252 not taken.
✗ Branch 251 → 253 not taken.
✗ Branch 255 → 256 not taken.
✗ Branch 255 → 257 not taken.
82 if (editScope == QStringLiteral("this") && !occurrenceStart.isValid())
692 ✗ return tr("The occurrence could not be identified.");
693 41 wasNew = uid.isEmpty();
694
2/2
✓ Branch 84 → 85 taken 31 times.
✓ Branch 84 → 129 taken 10 times.
41 if (!wasNew) {
695
2/2
✓ Branch 85 → 86 taken 3 times.
✓ Branch 85 → 116 taken 28 times.
31 if (editThisOccurrence) {
696 // Reuse the existing exception for this anchor if present.
697 const auto existing = sync->store()->occurrenceEvent(uid, accountId, calendarPath,
698
1/1
✓ Branch 87 → 88 taken 3 times.
3 occurrenceStart);
699
1/2
✗ Branch 89 → 90 not taken.
✓ Branch 89 → 92 taken 3 times.
3 if (existing) {
700 ✗ event = *existing;
701 } else {
702
1/1
✓ Branch 92 → 93 taken 3 times.
3 const auto master = sync->eventFor(uid, accountId, calendarPath);
703
1/3
✗ Branch 94 → 95 not taken.
✓ Branch 94 → 97 taken 3 times.
✗ Branch 95 → 96 not taken.
3 if (!master) return tr("The event no longer exists.");
704 3 event = *master;
705 3 event.id = 0;
706
1/1
✓ Branch 99 → 100 taken 3 times.
3 event.recurrenceId = occurrenceStart.toUTC();
707 3 event.rrule.clear();
708
1/1
✓ Branch 103 → 104 taken 3 times.
3 event.exdates.clear();
709 3 }
710
1/2
✓ Branch 112 → 113 taken 3 times.
✗ Branch 112 → 115 not taken.
3 } else {
711
1/1
✓ Branch 116 → 117 taken 28 times.
28 const auto stored = sync->eventFor(uid, accountId, calendarPath);
712
1/2
✗ Branch 118 → 119 not taken.
✓ Branch 118 → 121 taken 28 times.
28 if (!stored) {
713 ✗ return tr("The event no longer exists.");
714 }
715 28 event = *stored;
716 28 }
717 } else {
718
1/1
✓ Branch 129 → 130 taken 10 times.
10 const auto calendars = sync->calendars();
719
1/2
✗ Branch 131 → 132 not taken.
✓ Branch 131 → 134 taken 10 times.
10 if (calendars.isEmpty()) {
720 ✗ return tr("No calendar exists yet; create one in the DAV settings first.");
721 }
722 10 const CalendarInfo *target = &calendars.first();
723
1/1
✓ Branch 135 → 136 taken 10 times.
10 const QString hint = calendarPath.toLower();
724
1/2
✓ Branch 137 → 138 taken 10 times.
✗ Branch 137 → 159 not taken.
10 if (!hint.isEmpty()) {
725
1/2
✓ Branch 157 → 140 taken 11 times.
✗ Branch 157 → 158 not taken.
11 for (const auto &calendar : calendars)
726
5/7
✓ Branch 141 → 142 taken 11 times.
✓ Branch 143 → 144 taken 1 time.
✓ Branch 143 → 147 taken 10 times.
✓ Branch 153 → 154 taken 10 times.
✓ Branch 153 → 155 taken 1 time.
✗ Branch 273 → 274 not taken.
✗ Branch 273 → 275 not taken.
23 if (calendar.path.toLower() == hint ||
727
6/10
✓ Branch 144 → 145 taken 1 time.
✓ Branch 145 → 146 taken 1 time.
✗ Branch 146 → 147 not taken.
✓ Branch 146 → 148 taken 1 time.
✓ Branch 149 → 150 taken 1 time.
✓ Branch 149 → 151 taken 10 times.
✓ Branch 151 → 152 taken 11 times.
✗ Branch 151 → 153 not taken.
✗ Branch 269 → 270 not taken.
✗ Branch 269 → 271 not taken.
12 calendar.displayName.toLower().startsWith(hint)) {
728 10 target = &calendar;
729 10 break;
730 }
731 }
732
2/2
✓ Branch 159 → 160 taken 10 times.
✓ Branch 160 → 161 taken 10 times.
10 event.uid = QUuid::createUuid().toString(QUuid::WithoutBraces);
733 10 event.calendarPath = target->path;
734 10 event.accountId = target->accountId;
735 10 }
736 41 return {};
737 41 }
738
739 51 QVariantMap QuickPimBackend::saveEvent(const QVariantMap &fields) {
740
2/4
✓ Branch 8 → 9 taken 51 times.
✓ Branch 8 → 10 taken 51 times.
✗ Branch 489 → 490 not taken.
✗ Branch 489 → 491 not taken.
204 QVariantMap rejected{{QStringLiteral("ok"), false}};
741 51 auto *sync = m_session.calendarSync();
742
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 26 taken 51 times.
51 if (!sync) {
743 ✗ rejected.insert(QStringLiteral("error"), tr("No calendar storage available."));
744 ✗ return rejected;
745 }
746
3/3
✓ Branch 30 → 31 taken 51 times.
✓ Branch 31 → 32 taken 51 times.
✓ Branch 32 → 33 taken 51 times.
102 const QString summary = fields.value(QStringLiteral("summary")).toString().trimmed();
747
2/2
✓ Branch 39 → 40 taken 1 time.
✓ Branch 39 → 52 taken 50 times.
51 if (summary.isEmpty()) {
748
2/2
✓ Branch 40 → 41 taken 1 time.
✓ Branch 45 → 46 taken 1 time.
2 rejected.insert(QStringLiteral("error"), tr("Enter a subject."));
749 1 return rejected;
750 }
751
2/2
✓ Branch 56 → 57 taken 50 times.
✓ Branch 57 → 58 taken 50 times.
100 const bool allDay = fields.value(QStringLiteral("allDay")).toBool();
752
3/3
✓ Branch 66 → 67 taken 50 times.
✓ Branch 67 → 68 taken 50 times.
✓ Branch 68 → 69 taken 50 times.
100 const QDateTime start = PimEditorTime::parse(fields.value(QStringLiteral("start")).toString(), allDay);
753
2/2
✓ Branch 78 → 79 taken 50 times.
✓ Branch 79 → 80 taken 50 times.
100 const QString endText = fields.value(QStringLiteral("end")).toString();
754
4/4
✓ Branch 84 → 85 taken 50 times.
✓ Branch 86 → 87 taken 5 times.
✓ Branch 86 → 88 taken 45 times.
✓ Branch 88 → 89 taken 45 times.
50 const QDateTime end = endText.trimmed().isEmpty() ? QDateTime() : PimEditorTime::parse(endText, allDay);
755
9/15
✓ Branch 90 → 91 taken 50 times.
✓ Branch 91 → 92 taken 50 times.
✗ Branch 91 → 97 not taken.
✓ Branch 92 → 93 taken 50 times.
✓ Branch 94 → 95 taken 45 times.
✓ Branch 94 → 98 taken 5 times.
✓ Branch 95 → 96 taken 45 times.
✗ Branch 96 → 97 not taken.
✓ Branch 96 → 98 taken 45 times.
✓ Branch 99 → 100 taken 50 times.
✗ Branch 99 → 101 not taken.
✗ Branch 101 → 102 not taken.
✓ Branch 101 → 114 taken 50 times.
✗ Branch 586 → 587 not taken.
✗ Branch 586 → 588 not taken.
50 if (!start.isValid() || (!endText.trimmed().isEmpty() && !end.isValid())) {
756 ✗ rejected.insert(QStringLiteral("error"),
757 ✗ tr("Enter a valid date/time. Ambiguous daylight-saving times require an explicit UTC offset."));
758 ✗ return rejected;
759 }
760
8/8
✓ Branch 114 → 115 taken 50 times.
✓ Branch 115 → 116 taken 45 times.
✓ Branch 115 → 120 taken 5 times.
✓ Branch 116 → 117 taken 45 times.
✓ Branch 118 → 119 taken 3 times.
✓ Branch 118 → 120 taken 42 times.
✓ Branch 121 → 122 taken 3 times.
✓ Branch 121 → 134 taken 47 times.
50 if (end.isValid() && end < start) {
761
2/2
✓ Branch 122 → 123 taken 3 times.
✓ Branch 127 → 128 taken 3 times.
6 rejected.insert(QStringLiteral("error"), tr("The end cannot be before the start."));
762 3 return rejected;
763 }
764 47 QString storedRule;
765
3/3
✓ Branch 134 → 135 taken 47 times.
✓ Branch 136 → 137 taken 4 times.
✓ Branch 136 → 147 taken 43 times.
47 if (const QString error = eventRecurrenceRule(fields, storedRule); !error.isEmpty()) {
766
1/1
✓ Branch 141 → 142 taken 4 times.
8 rejected.insert(QStringLiteral("error"), error);
767 4 return rejected;
768 47 }
769 43 bool wasNew = false;
770 43 CalendarEvent event;
771 43 QList<ResourceSnapshot> undo;
772 43 QString seriesRule; // the rule before a split (S93.1.8)
773
2/2
✓ Branch 156 → 158 taken 43 times.
✓ Branch 158 → 159 taken 43 times.
86 const QString editScope = fields.value(QStringLiteral("editScope")).toString();
774 43 const QString error = editScope == QLatin1String("following")
775
2/2
✓ Branch 165 → 166 taken 2 times.
✓ Branch 165 → 167 taken 41 times.
43 ? splitSeriesForSave(sync, fields, event, wasNew, undo, &seriesRule)
776
2/2
✓ Branch 166 → 168 taken 2 times.
✓ Branch 167 → 168 taken 41 times.
43 : eventForSave(sync, fields, event, wasNew);
777
1/2
✗ Branch 169 → 170 not taken.
✓ Branch 169 → 180 taken 43 times.
43 if (!error.isEmpty()) {
778 ✗ rejected.insert(QStringLiteral("error"), error);
779 ✗ return rejected;
780 }
781
2/2
✓ Branch 180 → 181 taken 43 times.
✓ Branch 181 → 182 taken 43 times.
43 undo.append(snapshot(event.uid, event.accountId, event.calendarPath));
782 43 event.summary = summary;
783
2/2
✓ Branch 188 → 189 taken 43 times.
✓ Branch 189 → 190 taken 43 times.
86 event.description = fields.value(QStringLiteral("description")).toString();
784
3/3
✓ Branch 200 → 201 taken 43 times.
✓ Branch 201 → 202 taken 43 times.
✓ Branch 202 → 203 taken 43 times.
86 event.location = fields.value(QStringLiteral("location")).toString().trimmed();
785
8/9
✓ Branch 214 → 215 taken 43 times.
✓ Branch 218 → 219 taken 43 times.
✓ Branch 219 → 220 taken 1 time.
✓ Branch 219 → 223 taken 42 times.
✓ Branch 220 → 221 taken 1 time.
✓ Branch 221 → 222 taken 1 time.
✗ Branch 221 → 223 not taken.
✓ Branch 224 → 225 taken 1 time.
✓ Branch 224 → 304 taken 42 times.
86 if (const QVariant people = fields.value(QStringLiteral("people")); people.isValid() && people.canConvert<QVariantList>()) {
786
1/1
✓ Branch 225 → 226 taken 1 time.
1 event.people.clear();
787
5/5
✓ Branch 226 → 227 taken 1 time.
✓ Branch 227 → 228 taken 1 time.
✓ Branch 228 → 229 taken 1 time.
✓ Branch 301 → 230 taken 2 times.
✓ Branch 301 → 302 taken 1 time.
3 for (const QVariant &value : people.toList()) {
788
1/1
✓ Branch 231 → 232 taken 2 times.
2 const QVariantMap person = value.toMap();
789
2/2
✓ Branch 236 → 237 taken 2 times.
✓ Branch 237 → 238 taken 2 times.
4 const QString uid = person.value(QStringLiteral("uid")).toString();
790
3/3
✓ Branch 246 → 247 taken 2 times.
✓ Branch 247 → 248 taken 2 times.
✓ Branch 248 → 249 taken 2 times.
4 const QString name = person.value(QStringLiteral("name")).toString().trimmed();
791
1/2
✗ Branch 255 → 256 not taken.
✓ Branch 255 → 257 taken 2 times.
2 if (name.isEmpty())
792 ✗ continue;
793
13/25
✓ Branch 263 → 264 taken 1 time.
✓ Branch 273 → 274 taken 1 time.
✓ Branch 273 → 275 taken 1 time.
✓ Branch 275 → 276 taken 1 time.
✓ Branch 275 → 277 taken 1 time.
✓ Branch 277 → 278 taken 1 time.
✓ Branch 277 → 279 taken 1 time.
✓ Branch 279 → 280 taken 1 time.
✓ Branch 279 → 281 taken 1 time.
✓ Branch 281 → 282 taken 1 time.
✓ Branch 281 → 283 taken 1 time.
✓ Branch 283 → 284 taken 1 time.
✓ Branch 283 → 285 taken 1 time.
✗ Branch 724 → 725 not taken.
✗ Branch 724 → 726 not taken.
✗ Branch 728 → 729 not taken.
✗ Branch 728 → 730 not taken.
✗ Branch 732 → 733 not taken.
✗ Branch 732 → 734 not taken.
✗ Branch 736 → 737 not taken.
✗ Branch 736 → 738 not taken.
✗ Branch 740 → 741 not taken.
✗ Branch 740 → 742 not taken.
✗ Branch 744 → 745 not taken.
✗ Branch 744 → 746 not taken.
7 event.people.append({uid.isEmpty() ? person.value(QStringLiteral("uri")).toString() : QStringLiteral("urn:uuid:") + uid,
794 uid, name});
795 3 }
796 1 event.peopleSet = true; // S103.1.10: the writer applies the list only now
797 43 }
798 // S93.5.6: a new appointment from a mail starts from a VEVENT with the link.
799
8/8
✓ Branch 309 → 310 taken 43 times.
✓ Branch 310 → 311 taken 43 times.
✓ Branch 315 → 316 taken 12 times.
✓ Branch 315 → 319 taken 31 times.
✓ Branch 317 → 318 taken 1 time.
✓ Branch 317 → 319 taken 11 times.
✓ Branch 320 → 321 taken 1 time.
✓ Branch 320 → 325 taken 42 times.
86 if (const QString messageId = fields.value(QStringLiteral("messageId")).toString(); wasNew && !messageId.isEmpty())
800
1/1
✓ Branch 321 → 322 taken 1 time.
43 event.icalRaw = QuickMailEventLink::seedComponent(event.uid, messageId);
801 // S93.5.1: absent/undefined keeps the stored reminders (an edit that did
802 // not touch the reminder field never drops alarms set elsewhere).
803
1/1
✓ Branch 330 → 331 taken 43 times.
86 if (const QVariant reminders = fields.value(QStringLiteral("reminders"));
804
7/8
✓ Branch 334 → 335 taken 43 times.
✓ Branch 335 → 336 taken 17 times.
✓ Branch 335 → 339 taken 26 times.
✓ Branch 336 → 337 taken 17 times.
✓ Branch 337 → 338 taken 17 times.
✗ Branch 337 → 339 not taken.
✓ Branch 340 → 341 taken 17 times.
✓ Branch 340 → 365 taken 26 times.
43 reminders.isValid() && reminders.canConvert<QVariantList>()) {
805 17 QList<int> minutes;
806
5/5
✓ Branch 341 → 342 taken 17 times.
✓ Branch 342 → 343 taken 17 times.
✓ Branch 343 → 344 taken 17 times.
✓ Branch 357 → 345 taken 9 times.
✓ Branch 357 → 358 taken 17 times.
26 for (const QVariant &value : reminders.toList())
807
5/8
✓ Branch 346 → 347 taken 9 times.
✓ Branch 348 → 349 taken 9 times.
✗ Branch 348 → 352 not taken.
✓ Branch 350 → 351 taken 9 times.
✗ Branch 350 → 352 not taken.
✓ Branch 353 → 354 taken 9 times.
✗ Branch 353 → 355 not taken.
✓ Branch 354 → 355 taken 9 times.
26 if (const int m = value.toInt(); !minutes.contains(m) && qAbs(m) <= 7 * 1440) minutes.append(m);
808
3/3
✓ Branch 359 → 360 taken 17 times.
✓ Branch 360 → 361 taken 17 times.
✓ Branch 361 → 362 taken 17 times.
17 std::sort(minutes.begin(), minutes.end());
809 17 event.reminders = minutes;
810 60 }
811 // A split keeps its adjusted rule (reduced COUNT) unless the user
812 // changed the recurrence in the editor.
813
10/11
✓ Branch 366 → 367 taken 43 times.
✓ Branch 367 → 368 taken 40 times.
✓ Branch 367 → 374 taken 3 times.
✓ Branch 368 → 369 taken 12 times.
✓ Branch 368 → 373 taken 28 times.
✓ Branch 370 → 371 taken 2 times.
✓ Branch 370 → 373 taken 10 times.
✗ Branch 372 → 373 not taken.
✓ Branch 372 → 374 taken 2 times.
✓ Branch 375 → 376 taken 38 times.
✓ Branch 375 → 377 taken 5 times.
43 if (!event.recurrenceId.isValid() && !(wasNew && !seriesRule.isEmpty() && storedRule == seriesRule))
814 38 event.rrule = storedRule;
815 43 event.allDay = allDay;
816
2/2
✓ Branch 377 → 378 taken 3 times.
✓ Branch 377 → 397 taken 40 times.
43 if (allDay) {
817 // S93.1.7: inclusive end date in the editor, exclusive DTEND stored;
818 // no end means one day.
819
4/4
✓ Branch 378 → 379 taken 3 times.
✓ Branch 379 → 380 taken 3 times.
✓ Branch 380 → 381 taken 3 times.
✓ Branch 381 → 382 taken 3 times.
3 event.dtStart = QDateTime(start.date(), QTime(0, 0), QTimeZone::utc());
820
5/5
✓ Branch 385 → 386 taken 3 times.
✓ Branch 386 → 387 taken 2 times.
✓ Branch 386 → 388 taken 1 time.
✓ Branch 387 → 389 taken 2 times.
✓ Branch 388 → 389 taken 1 time.
3 const QDate lastDay = end.isValid() ? end.date() : start.date();
821
4/4
✓ Branch 389 → 390 taken 3 times.
✓ Branch 390 → 391 taken 3 times.
✓ Branch 391 → 392 taken 3 times.
✓ Branch 392 → 393 taken 3 times.
3 event.dtEnd = QDateTime(lastDay.addDays(1), QTime(0, 0), QTimeZone::utc());
822 } else {
823 // S93.1.2: keep the event's zone when the instant did not change (a
824 // title edit), otherwise the editor value carries the local zone.
825
1/1
✓ Branch 397 → 398 taken 40 times.
40 event.dtStart = sameInstantKeepZone(event.dtStart, start);
826
3/4
✓ Branch 400 → 401 taken 40 times.
✓ Branch 401 → 402 taken 40 times.
✗ Branch 401 → 403 not taken.
✓ Branch 402 → 404 taken 40 times.
40 event.dtEnd = end.isValid() ? sameInstantKeepZone(event.dtEnd, end) : QDateTime();
827 }
828 // S103.6.1: the people live in the component (CONTACT lines, no column):
829 // an event without a stored VEVENT gets one now, written from its fields.
830 // The zones come from the stored resource's VTIMEZONEs, as on upload.
831
2/2
✓ Branch 407 → 408 taken 1 time.
✓ Branch 407 → 428 taken 42 times.
43 if (event.peopleSet)
832
2/2
✓ Branch 416 → 417 taken 1 time.
✓ Branch 417 → 418 taken 1 time.
2 event.icalRaw = ICalEventCodec::componentText(ICalEventCodec::eventComponent(
833 2 event, ICalTime::ZoneResolver(event.resourceIcal.isEmpty()
834
1/3
✓ Branch 409 → 410 taken 1 time.
✗ Branch 409 → 411 not taken.
✗ Branch 414 → 415 not taken.
2 ? ICal::Component{}
835
3/9
✓ Branch 415 → 416 taken 1 time.
✗ Branch 423 → 424 not taken.
✓ Branch 423 → 425 taken 1 time.
✗ Branch 425 → 426 not taken.
✓ Branch 425 → 427 taken 1 time.
✗ Branch 816 → 817 not taken.
✗ Branch 816 → 818 not taken.
✗ Branch 820 → 821 not taken.
✗ Branch 820 → 822 not taken.
3 : ICal::parseFirst(event.resourceIcal, QStringLiteral("VCALENDAR")))));
836
2/3
✓ Branch 428 → 429 taken 43 times.
✗ Branch 429 → 430 not taken.
✓ Branch 429 → 442 taken 43 times.
43 if (!sync->saveEvent(event, wasNew)) {
837 ✗ rejected.insert(QStringLiteral("error"), tr("The local calendar write failed."));
838 ✗ return rejected;
839 }
840
5/5
✓ Branch 442 → 443 taken 12 times.
✓ Branch 442 → 444 taken 31 times.
✓ Branch 443 → 445 taken 12 times.
✓ Branch 444 → 445 taken 31 times.
✓ Branch 445 → 446 taken 43 times.
43 pushUndo(wasNew ? tr("Appointment created") : tr("Appointment changed"), undo);
841
2/4
✓ Branch 464 → 465 taken 129 times.
✓ Branch 464 → 466 taken 43 times.
✗ Branch 844 → 845 not taken.
✗ Branch 844 → 846 not taken.
387 return {{QStringLiteral("ok"), true}, {QStringLiteral("stage"), QStringLiteral("savedLocal")}, {QStringLiteral("uid"), event.uid}};
842 423 }
843
844 34 QVariantMap QuickPimBackend::eventPeople(const QString &uid, const QString &accountId,
845 const QString &calendarPath) const {
846 34 CalendarSyncCoordinator *sync = m_session.calendarSync();
847
2/3
✓ Branch 3 → 4 taken 34 times.
✗ Branch 3 → 5 not taken.
✓ Branch 4 → 6 taken 34 times.
34 const auto event = sync ? sync->eventFor(uid, accountId, calendarPath) : std::nullopt;
848
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 34 times.
34 if (!event)
849 ✗ return {};
850
1/1
✓ Branch 13 → 14 taken 34 times.
34 const ICal::Component vevent = ICal::parseFirst(event->icalRaw, QStringLiteral("VEVENT"));
851 34 QVariantList people, attendees;
852
5/5
✓ Branch 16 → 17 taken 34 times.
✓ Branch 17 → 18 taken 34 times.
✓ Branch 18 → 19 taken 34 times.
✓ Branch 49 → 20 taken 2 times.
✓ Branch 49 → 50 taken 34 times.
36 for (const EventPerson &person : ICalEventCodec::peopleOf(vevent))
853
1/1
✓ Branch 35 → 36 taken 2 times.
6 people.append(QVariantMap{{QStringLiteral("uid"), person.uid},
854 4 {QStringLiteral("uri"), person.uri},
855
2/4
✓ Branch 39 → 40 taken 6 times.
✓ Branch 39 → 41 taken 2 times.
✗ Branch 127 → 128 not taken.
✗ Branch 127 → 129 not taken.
46 {QStringLiteral("name"), person.name}});
856
5/5
✓ Branch 51 → 52 taken 34 times.
✓ Branch 52 → 53 taken 34 times.
✓ Branch 53 → 54 taken 34 times.
✓ Branch 92 → 55 taken 25 times.
✓ Branch 92 → 93 taken 34 times.
59 for (const EventAttendee &attendee : ICalEventCodec::attendeesOf(vevent))
857
1/2
✓ Branch 58 → 59 taken 25 times.
✗ Branch 58 → 90 not taken.
25 if (attendee.cutype.compare(QLatin1String("ROOM"), Qt::CaseInsensitive) != 0)
858
1/1
✓ Branch 77 → 78 taken 25 times.
75 attendees.append(QVariantMap{{QStringLiteral("name"), attendee.name.isEmpty() ? attendee.address : attendee.name},
859 50 {QStringLiteral("address"), attendee.address},
860
2/4
✓ Branch 81 → 82 taken 75 times.
✓ Branch 81 → 83 taken 25 times.
✗ Branch 164 → 165 not taken.
✗ Branch 164 → 166 not taken.
184 {QStringLiteral("organizer"), attendee.organizer}});
861
2/4
✓ Branch 104 → 105 taken 68 times.
✓ Branch 104 → 106 taken 34 times.
✗ Branch 197 → 198 not taken.
✗ Branch 197 → 199 not taken.
170 return {{QStringLiteral("people"), people}, {QStringLiteral("attendees"), attendees}};
862 244 }
863
864 // saveEvent step for "this and all following": ends the stored series before
865 // the edited occurrence and returns the remainder as a new series (new UID)
866 // for the caller to fill with the edited fields.
867 2 QString QuickPimBackend::splitSeriesForSave(CalendarSyncCoordinator *sync, const QVariantMap &fields,
868 CalendarEvent &event, bool &wasNew,
869 QList<ResourceSnapshot> &undo, QString *seriesRule) const {
870
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 7 → 8 taken 2 times.
4 const QString uid = fields.value(QStringLiteral("uid")).toString();
871
2/2
✓ Branch 16 → 17 taken 2 times.
✓ Branch 17 → 18 taken 2 times.
4 const QString accountId = fields.value(QStringLiteral("accountId")).toString();
872
2/2
✓ Branch 26 → 27 taken 2 times.
✓ Branch 27 → 28 taken 2 times.
4 const QString calendarPath = fields.value(QStringLiteral("calendarPath")).toString();
873 const QDateTime anchor = QDateTime::fromString(
874
3/3
✓ Branch 36 → 37 taken 2 times.
✓ Branch 37 → 38 taken 2 times.
✓ Branch 38 → 39 taken 2 times.
4 fields.value(QStringLiteral("occurrenceStart")).toString(), Qt::ISODate);
875
1/1
✓ Branch 44 → 45 taken 2 times.
2 const auto master = sync->eventFor(uid, accountId, calendarPath);
876
1/2
✗ Branch 46 → 47 not taken.
✓ Branch 46 → 49 taken 2 times.
2 if (!master)
877 ✗ return tr("The event no longer exists.");
878
2/3
✓ Branch 49 → 50 taken 2 times.
✗ Branch 50 → 51 not taken.
✓ Branch 50 → 53 taken 2 times.
2 if (!anchor.isValid())
879 ✗ return tr("The occurrence could not be identified.");
880 2 *seriesRule = master->rrule;
881
2/3
✓ Branch 56 → 57 taken 2 times.
✗ Branch 57 → 58 not taken.
✓ Branch 57 → 60 taken 2 times.
2 if (!CalendarSeries::splittableAt(*master, anchor))
882 ✗ return tr("This series is too long to be split at this appointment. Edit the whole series instead.");
883 2 CalendarEvent ended = *master;
884
2/3
✓ Branch 62 → 63 taken 2 times.
✗ Branch 63 → 64 not taken.
✓ Branch 63 → 67 taken 2 times.
2 if (!CalendarSeries::endBefore(&ended, anchor)) {
885 // The first appointment of the series: "following" means all of them.
886 ✗ event = *master;
887 ✗ wasNew = false;
888 ✗ return {};
889 }
890
2/2
✓ Branch 67 → 68 taken 2 times.
✓ Branch 68 → 69 taken 2 times.
2 undo.append(snapshot(uid, accountId, calendarPath));
891 // Exceptions from the anchor on belong to the part that is replaced.
892
5/5
✓ Branch 71 → 72 taken 2 times.
✓ Branch 72 → 73 taken 2 times.
✓ Branch 73 → 74 taken 2 times.
✓ Branch 94 → 75 taken 2 times.
✓ Branch 94 → 95 taken 2 times.
4 for (const CalendarEvent &row : sync->store()->eventSeries(uid, accountId, calendarPath))
893
5/18
✓ Branch 76 → 77 taken 2 times.
✗ Branch 77 → 78 not taken.
✓ Branch 77 → 84 taken 2 times.
✗ Branch 78 → 79 not taken.
✗ Branch 79 → 80 not taken.
✗ Branch 80 → 81 not taken.
✗ Branch 82 → 83 not taken.
✗ Branch 82 → 84 not taken.
✗ Branch 85 → 86 not taken.
✓ Branch 85 → 87 taken 2 times.
✗ Branch 87 → 88 not taken.
✓ Branch 87 → 89 taken 2 times.
✗ Branch 89 → 90 not taken.
✓ Branch 89 → 92 taken 2 times.
✗ Branch 184 → 185 not taken.
✗ Branch 184 → 186 not taken.
✗ Branch 188 → 189 not taken.
✗ Branch 188 → 190 not taken.
2 if (row.recurrenceId.isValid() && row.recurrenceId.toUTC() >= anchor.toUTC())
894
0/1
✗ Branch 91 → 92 not taken.
2 sync->store()->deleteEvent(uid, accountId, calendarPath, row.recurrenceId);
895
2/3
✓ Branch 96 → 97 taken 2 times.
✗ Branch 97 → 98 not taken.
✓ Branch 97 → 114 taken 2 times.
2 if (!sync->saveEvent(ended, false)) {
896 // S106-303: the deleted exceptions come back with the failed split.
897 ✗ const bool restored = sync->restoreEventSeries(uid, accountId, calendarPath, undo.last().rows);
898 ✗ undo.removeLast();
899 ✗ qCWarning(lcPimBackend) << "Series split failed; exceptions restored:" << restored;
900 ✗ return tr("The local calendar write failed.");
901 }
902
1/1
✓ Branch 117 → 118 taken 2 times.
4 event = CalendarSeries::followingPart(*master, anchor,
903
2/2
✓ Branch 114 → 115 taken 2 times.
✓ Branch 115 → 116 taken 2 times.
6 QUuid::createUuid().toString(QUuid::WithoutBraces));
904 2 wasNew = true;
905 2 return {};
906 2 }
907
908 61 QuickPimBackend::ResourceSnapshot QuickPimBackend::snapshot(const QString &uid, const QString &accountId,
909 const QString &calendarPath) const {
910 61 ResourceSnapshot result{uid, accountId, calendarPath, {}};
911
3/6
✓ Branch 6 → 7 taken 61 times.
✗ Branch 6 → 10 not taken.
✓ Branch 8 → 9 taken 61 times.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 61 times.
✗ Branch 11 → 17 not taken.
61 if (const auto *sync = m_session.calendarSync(); sync && sync->store())
912
1/1
✓ Branch 13 → 14 taken 61 times.
61 result.rows = sync->store()->eventSeries(uid, accountId, calendarPath);
913 61 return result;
914 ✗ }
915
916 59 void QuickPimBackend::pushUndo(const QString &text, const QList<ResourceSnapshot> &resources) {
917 59 m_calendarUndo.append({text, resources});
918
1/2
✗ Branch 9 → 7 not taken.
✓ Branch 9 → 10 taken 59 times.
59 while (m_calendarUndo.size() > 20)
919 ✗ m_calendarUndo.removeFirst();
920 59 emit calendarUndoChanged();
921 118 }
922
923 8 QString QuickPimBackend::calendarUndoText() const {
924
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 8 times.
8 return m_calendarUndo.isEmpty() ? QString() : m_calendarUndo.last().text;
925 }
926
927 14 bool QuickPimBackend::undoCalendarChange() {
928 14 auto *sync = m_session.calendarSync();
929
5/6
✓ Branch 3 → 4 taken 14 times.
✗ Branch 3 → 6 not taken.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 13 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 13 times.
14 if (!sync || m_calendarUndo.isEmpty())
930 1 return false;
931
1/1
✓ Branch 10 → 11 taken 13 times.
13 const CalendarUndo undo = m_calendarUndo.takeLast();
932 13 bool ok = true;
933 // Newest resource first: a split restores the old series last.
934
3/3
✓ Branch 23 → 24 taken 27 times.
✓ Branch 24 → 12 taken 14 times.
✓ Branch 24 → 25 taken 13 times.
27 for (auto it = undo.resources.crbegin(); it != undo.resources.crend(); ++it)
935
8/10
✓ Branch 12 → 13 taken 14 times.
✓ Branch 13 → 14 taken 14 times.
✓ Branch 14 → 15 taken 14 times.
✓ Branch 15 → 16 taken 14 times.
✓ Branch 16 → 17 taken 14 times.
✓ Branch 17 → 18 taken 14 times.
✗ Branch 17 → 20 not taken.
✓ Branch 18 → 19 taken 14 times.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 14 times.
14 ok = sync->restoreEventSeries(it->uid, it->accountId, it->calendarPath, it->rows) && ok;
936
1/1
✓ Branch 25 → 26 taken 13 times.
13 emit calendarUndoChanged();
937
5/10
✓ Branch 26 → 27 taken 13 times.
✗ Branch 26 → 30 not taken.
✓ Branch 27 → 28 taken 13 times.
✓ Branch 29 → 31 taken 13 times.
✗ Branch 30 → 31 not taken.
✓ Branch 31 → 32 taken 13 times.
✓ Branch 33 → 34 taken 13 times.
✗ Branch 33 → 35 not taken.
✗ Branch 43 → 44 not taken.
✗ Branch 43 → 45 not taken.
13 m_deps.setStatus(ok ? tr("Undone: %1").arg(undo.text) : tr("The change could not be undone."));
938 13 return ok;
939 13 }
940
941 15 QVariantMap QuickPimBackend::removeEventScoped(const QVariantMap &occurrence, const QString &scope) {
942
2/4
✓ Branch 8 → 9 taken 15 times.
✓ Branch 8 → 10 taken 15 times.
✗ Branch 282 → 283 not taken.
✗ Branch 282 → 284 not taken.
60 QVariantMap rejected{{QStringLiteral("ok"), false}};
943 15 auto *sync = m_session.calendarSync();
944
2/2
✓ Branch 17 → 18 taken 15 times.
✓ Branch 18 → 19 taken 15 times.
30 const QString uid = occurrence.value(QStringLiteral("uid")).toString();
945
2/2
✓ Branch 27 → 28 taken 15 times.
✓ Branch 28 → 29 taken 15 times.
30 const QString accountId = occurrence.value(QStringLiteral("accountId")).toString();
946
2/2
✓ Branch 37 → 38 taken 15 times.
✓ Branch 38 → 39 taken 15 times.
30 const QString calendarPath = occurrence.value(QStringLiteral("calendarPath")).toString();
947
3/6
✓ Branch 43 → 44 taken 15 times.
✗ Branch 43 → 46 not taken.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 15 times.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 61 taken 15 times.
15 if (!sync || !sync->store()) {
948 ✗ rejected.insert(QStringLiteral("error"), tr("No calendar storage available."));
949 ✗ return rejected;
950 }
951
1/1
✓ Branch 61 → 62 taken 15 times.
15 const auto calendars = sync->calendars();
952
2/3
✓ Branch 64 → 65 taken 15 times.
✗ Branch 65 → 66 not taken.
✓ Branch 65 → 78 taken 15 times.
15 if (std::none_of(calendars.cbegin(), calendars.cend(), [&](const CalendarInfo &calendar) {
953
3/4
✓ Branch 3 → 4 taken 21 times.
✗ Branch 3 → 7 not taken.
✓ Branch 5 → 6 taken 15 times.
✓ Branch 5 → 7 taken 6 times.
21 return calendar.accountId == accountId && calendar.path == calendarPath;
954 })) {
955 ✗ rejected.insert(QStringLiteral("error"), tr("This calendar is read-only."));
956 ✗ return rejected;
957 }
958
1/1
✓ Branch 78 → 79 taken 15 times.
15 const auto master = sync->eventFor(uid, accountId, calendarPath);
959
1/2
✗ Branch 80 → 81 not taken.
✓ Branch 80 → 93 taken 15 times.
15 if (!master) {
960 ✗ rejected.insert(QStringLiteral("error"), tr("The event no longer exists."));
961 ✗ return rejected;
962 }
963
2/2
✓ Branch 97 → 98 taken 15 times.
✓ Branch 98 → 99 taken 15 times.
30 const QString recurrenceIso = occurrence.value(QStringLiteral("recurrenceIso")).toString();
964 const QDateTime anchor = QDateTime::fromString(
965
12/20
✓ Branch 104 → 105 taken 14 times.
✓ Branch 104 → 111 taken 1 time.
✓ Branch 109 → 110 taken 14 times.
✓ Branch 110 → 112 taken 14 times.
✓ Branch 114 → 115 taken 14 times.
✓ Branch 114 → 116 taken 1 time.
✓ Branch 116 → 117 taken 14 times.
✓ Branch 116 → 118 taken 1 time.
✓ Branch 118 → 119 taken 14 times.
✓ Branch 118 → 120 taken 1 time.
✓ Branch 120 → 121 taken 14 times.
✓ Branch 120 → 122 taken 1 time.
✗ Branch 384 → 385 not taken.
✗ Branch 384 → 386 not taken.
✗ Branch 388 → 389 not taken.
✗ Branch 388 → 390 not taken.
✗ Branch 392 → 393 not taken.
✗ Branch 392 → 394 not taken.
✗ Branch 396 → 397 not taken.
✗ Branch 396 → 398 not taken.
44 recurrenceIso.isEmpty() ? occurrence.value(QStringLiteral("startIso")).toString() : recurrenceIso,
966
1/1
✓ Branch 112 → 113 taken 15 times.
15 Qt::ISODate);
967
1/1
✓ Branch 122 → 123 taken 15 times.
15 const ResourceSnapshot before = snapshot(uid, accountId, calendarPath);
968 15 QString text;
969 15 bool ok = false;
970
7/7
✓ Branch 124 → 125 taken 15 times.
✓ Branch 125 → 126 taken 6 times.
✓ Branch 125 → 129 taken 9 times.
✓ Branch 128 → 129 taken 1 time.
✓ Branch 128 → 130 taken 5 times.
✓ Branch 131 → 132 taken 10 times.
✓ Branch 131 → 137 taken 5 times.
15 if (!CalendarSeries::isRecurring(*master) || scope == QLatin1String("all")) {
971
1/1
✓ Branch 132 → 133 taken 10 times.
10 ok = sync->removeEvent(uid, accountId, calendarPath);
972
1/1
✓ Branch 133 → 134 taken 10 times.
10 text = tr("Appointment deleted");
973
6/7
✓ Branch 139 → 140 taken 4 times.
✓ Branch 139 → 143 taken 1 time.
✓ Branch 140 → 141 taken 4 times.
✓ Branch 141 → 142 taken 4 times.
✗ Branch 141 → 143 not taken.
✓ Branch 144 → 145 taken 4 times.
✓ Branch 144 → 165 taken 1 time.
5 } else if (scope == QLatin1String("this") && anchor.isValid()) {
974 4 CalendarEvent event = *master;
975
2/2
✓ Branch 148 → 149 taken 1 time.
✓ Branch 148 → 151 taken 3 times.
4 if (!recurrenceIso.isEmpty())
976
1/1
✓ Branch 150 → 151 taken 1 time.
1 sync->store()->deleteEvent(uid, accountId, calendarPath, anchor);
977
1/1
✓ Branch 153 → 154 taken 4 times.
4 const bool known = std::any_of(event.exdates.cbegin(), event.exdates.cend(),
978 ✗ [&anchor](const QDateTime &value) { return value.toUTC() == anchor.toUTC(); });
979
1/2
✓ Branch 154 → 155 taken 4 times.
✗ Branch 154 → 159 not taken.
4 if (!known)
980
2/2
✓ Branch 155 → 156 taken 4 times.
✓ Branch 156 → 157 taken 4 times.
4 event.exdates.append(anchor.toUTC());
981
1/1
✓ Branch 159 → 160 taken 4 times.
4 ok = sync->saveEvent(event, false);
982
1/1
✓ Branch 160 → 161 taken 4 times.
4 text = tr("Appointment deleted");
983
4/7
✓ Branch 167 → 168 taken 1 time.
✗ Branch 167 → 171 not taken.
✓ Branch 168 → 169 taken 1 time.
✓ Branch 169 → 170 taken 1 time.
✗ Branch 169 → 171 not taken.
✓ Branch 172 → 173 taken 1 time.
✗ Branch 172 → 208 not taken.
5 } else if (scope == QLatin1String("following") && anchor.isValid()) {
984 1 CalendarEvent event = *master;
985
2/3
✓ Branch 175 → 176 taken 1 time.
✗ Branch 176 → 177 not taken.
✓ Branch 176 → 179 taken 1 time.
1 if (!CalendarSeries::endBefore(&event, anchor)) {
986 ✗ ok = sync->removeEvent(uid, accountId, calendarPath);
987 } else {
988
2/2
✓ Branch 200 → 181 taken 1 time.
✓ Branch 200 → 201 taken 1 time.
2 for (const CalendarEvent &row : before.rows)
989
5/18
✓ Branch 182 → 183 taken 1 time.
✗ Branch 183 → 184 not taken.
✓ Branch 183 → 190 taken 1 time.
✗ Branch 184 → 185 not taken.
✗ Branch 185 → 186 not taken.
✗ Branch 186 → 187 not taken.
✗ Branch 188 → 189 not taken.
✗ Branch 188 → 190 not taken.
✗ Branch 191 → 192 not taken.
✓ Branch 191 → 193 taken 1 time.
✗ Branch 193 → 194 not taken.
✓ Branch 193 → 195 taken 1 time.
✗ Branch 195 → 196 not taken.
✓ Branch 195 → 198 taken 1 time.
✗ Branch 412 → 413 not taken.
✗ Branch 412 → 414 not taken.
✗ Branch 416 → 417 not taken.
✗ Branch 416 → 418 not taken.
1 if (row.recurrenceId.isValid() && row.recurrenceId.toUTC() >= anchor.toUTC())
990 ✗ sync->store()->deleteEvent(uid, accountId, calendarPath, row.recurrenceId);
991
1/1
✓ Branch 201 → 202 taken 1 time.
1 ok = sync->saveEvent(event, false);
992 }
993
1/1
✓ Branch 203 → 204 taken 1 time.
1 text = tr("This and all following appointments deleted");
994 1 } else {
995 ✗ rejected.insert(QStringLiteral("error"), tr("Choose which appointments to delete."));
996 ✗ return rejected;
997 }
998
2/2
✓ Branch 220 → 221 taken 1 time.
✓ Branch 220 → 245 taken 14 times.
15 if (!ok) {
999 // S106-303: exceptions deleted before the failed save come back.
1000
1/1
✓ Branch 221 → 222 taken 1 time.
1 const bool restored = sync->restoreEventSeries(uid, accountId, calendarPath, before.rows);
1001
6/6
✓ Branch 222 → 223 taken 1 time.
✓ Branch 226 → 227 taken 1 time.
✓ Branch 227 → 228 taken 1 time.
✓ Branch 228 → 229 taken 1 time.
✓ Branch 232 → 224 taken 1 time.
✓ Branch 232 → 233 taken 1 time.
2 qCWarning(lcPimBackend) << "Scoped delete failed; series restored:" << restored;
1002
2/2
✓ Branch 233 → 234 taken 1 time.
✓ Branch 238 → 239 taken 1 time.
2 rejected.insert(QStringLiteral("error"), tr("The appointment could not be deleted."));
1003 1 return rejected;
1004 }
1005
3/5
✓ Branch 247 → 248 taken 14 times.
✓ Branch 250 → 251 taken 14 times.
✓ Branch 250 → 252 taken 14 times.
✗ Branch 457 → 458 not taken.
✗ Branch 457 → 459 not taken.
28 pushUndo(text, {before});
1006
2/4
✓ Branch 262 → 263 taken 28 times.
✓ Branch 262 → 264 taken 14 times.
✗ Branch 468 → 469 not taken.
✗ Branch 468 → 470 not taken.
84 return {{QStringLiteral("ok"), true}, {QStringLiteral("undoText"), text}};
1007 130 }
1008
1009 1 QVariantMap QuickPimBackend::eventOccurrenceAt(
1010 const QString &uid, const QString &accountId, const QString &calendarPath,
1011 const QString &recurrenceId) const {
1012 1 auto *sync = m_session.calendarSync();
1013
3/6
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1 time.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
1 if (!sync || !sync->store()) return {};
1014
1/1
✓ Branch 10 → 11 taken 1 time.
1 const QDateTime anchor = QDateTime::fromString(recurrenceId, Qt::ISODate);
1015
2/3
✓ Branch 11 → 12 taken 1 time.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 1 time.
1 if (!anchor.isValid()) return {};
1016
1/1
✓ Branch 15 → 16 taken 1 time.
1 const auto candidate = sync->store()->occurrenceEvent(uid, accountId, calendarPath, anchor);
1017
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 1 time.
1 if (!candidate) return {};
1018
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
1 QVariantMap fields = editorFields(*candidate, describeRecurrence(QString(), QString()));
1019
3/3
✓ Branch 28 → 29 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 34 → 35 taken 1 time.
2 fields.insert(QStringLiteral("recurrenceId"), candidate->recurrenceId.toUTC().toString(Qt::ISODate));
1020
1/1
✓ Branch 44 → 45 taken 1 time.
2 fields.insert(QStringLiteral("recurring"), false);
1021 1 return fields;
1022 1 }
1023
1024 1 QVariantMap QuickPimBackend::removeEventOccurrence(const QString &uid,
1025 const QString &accountId,
1026 const QString &calendarPath,
1027 const QString &occurrenceStart) {
1028
2/4
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 1 time.
✗ Branch 125 → 126 not taken.
✗ Branch 125 → 127 not taken.
4 QVariantMap rejected{{QStringLiteral("ok"), false}};
1029 1 auto *sync = m_session.calendarSync();
1030
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 26 taken 1 time.
1 if (!sync) {
1031 ✗ rejected.insert(QStringLiteral("error"), tr("No calendar storage available."));
1032 ✗ return rejected;
1033 }
1034
1/1
✓ Branch 26 → 27 taken 1 time.
1 const auto master = sync->eventFor(uid, accountId, calendarPath);
1035
1/2
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 41 taken 1 time.
1 if (!master) {
1036 ✗ rejected.insert(QStringLiteral("error"), tr("The event no longer exists."));
1037 ✗ return rejected;
1038 }
1039
2/3
✓ Branch 42 → 43 taken 1 time.
✓ Branch 45 → 46 taken 1 time.
✗ Branch 45 → 58 not taken.
1 if (master->rrule.trimmed().isEmpty()) {
1040
1/1
✓ Branch 51 → 52 taken 1 time.
2 rejected.insert(QStringLiteral("error"),
1041
1/1
✓ Branch 46 → 47 taken 1 time.
2 tr("The event is not a series; delete the event itself."));
1042 1 return rejected;
1043 }
1044 ✗ const QDateTime occurrence = QDateTime::fromString(occurrenceStart, Qt::ISODate);
1045 ✗ if (!occurrence.isValid()) {
1046 ✗ rejected.insert(QStringLiteral("error"), tr("Unknown occurrence."));
1047 ✗ return rejected;
1048 }
1049 ✗ const QDateTime utcOccurrence = occurrence.toUTC();
1050 ✗ CalendarEvent event = *master;
1051 ✗ const bool duplicate = std::any_of(event.exdates.cbegin(), event.exdates.cend(),
1052 ✗ [&utcOccurrence](const QDateTime &existing) {
1053 ✗ return existing.toUTC() == utcOccurrence;
1054 });
1055 ✗ if (!duplicate) event.exdates.append(utcOccurrence);
1056 ✗ const ResourceSnapshot before = snapshot(uid, accountId, calendarPath);
1057 ✗ if (!sync->saveEvent(event, false)) {
1058 ✗ rejected.insert(QStringLiteral("error"),
1059 ✗ tr("The occurrence could not be deleted."));
1060 ✗ return rejected;
1061 }
1062 ✗ pushUndo(tr("Appointment deleted"), {before});
1063 ✗ return {{QStringLiteral("ok"), true}};
1064 4 }
1065
1066 1 bool QuickPimBackend::removeEvent(const QString &uid, const QString &accountId,
1067 const QString &calendarPath) {
1068 1 auto *sync = m_session.calendarSync();
1069
3/6
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1 time.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
1 if (!sync || uid.isEmpty()) return false;
1070
1/1
✓ Branch 10 → 11 taken 1 time.
1 const ResourceSnapshot before = snapshot(uid, accountId, calendarPath);
1071
2/3
✓ Branch 11 → 12 taken 1 time.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 1 time.
1 if (!sync->removeEvent(uid, accountId, calendarPath))
1072 ✗ return false;
1073
4/6
✓ Branch 16 → 17 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 1 time.
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
2 pushUndo(tr("Appointment deleted"), {before});
1074 1 return true;
1075 2 }
1076
1077 // ─── S93.4.1 details popover ───
1078
1079 namespace {
1080 struct OccurrenceRef {
1081 QString uid, accountId, calendarPath, recurrenceIso, startIso;
1082 };
1083 62 OccurrenceRef refOf(const QVariantMap &occurrence) {
1084
1/1
✓ Branch 6 → 7 taken 62 times.
186 return {occurrence.value(QStringLiteral("uid")).toString(),
1085
1/1
✓ Branch 12 → 13 taken 62 times.
124 occurrence.value(QStringLiteral("accountId")).toString(),
1086
1/1
✓ Branch 18 → 19 taken 62 times.
124 occurrence.value(QStringLiteral("calendarPath")).toString(),
1087
1/1
✓ Branch 24 → 25 taken 62 times.
124 occurrence.value(QStringLiteral("recurrenceIso")).toString(),
1088
1/1
✓ Branch 30 → 31 taken 62 times.
186 occurrence.value(QStringLiteral("startIso")).toString()};
1089 310 }
1090 } // namespace
1091
1092 // S93.4.4: drag & drop and Alt+arrow moves. The new times go through
1093 // saveEvent (validation, outbox, undo); scope "this" moves one occurrence
1094 // of a series (an exception), "series" shifts the whole series.
1095 namespace {
1096 // F-45 (SPRINT-104): when a series start moves by whole days, the rule
1097 // parts that pinned the OLD start move with it — a weekly BYDAY=MO series
1098 // dragged to Tuesday becomes BYDAY=TU (RFC 5545 counts DTSTART as the
1099 // first occurrence, so an unshifted rule showed one Tuesday and kept
1100 // every later date on Monday). Monthly BYMONTHDAY shifts by the same day
1101 // distance; an UNTIL bound moves with the series. Rules without such
1102 // anchors (a bare FREQ=WEEKLY whose DTSTART carries the weekday) need
1103 // nothing — the moved start already carries it.
1104 5 QString shiftSeriesRuleAnchors(const QString &rule, int dayDelta) {
1105
7/11
✓ Branch 2 → 3 taken 5 times.
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 3 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 2 times.
✓ Branch 8 → 9 taken 5 times.
✗ Branch 8 → 10 not taken.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 12 taken 2 times.
✗ Branch 96 → 97 not taken.
✗ Branch 96 → 98 not taken.
5 if (rule.trimmed().isEmpty() || dayDelta == 0)
1106 3 return rule;
1107
1/1
✓ Branch 12 → 13 taken 2 times.
2 RecurrenceRule parsed = RecurrenceRule::parse(rule);
1108
2/4
✓ Branch 13 → 14 taken 2 times.
✗ Branch 13 → 15 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 2 times.
2 if (!parsed.valid || parsed.frequency == RecurrenceRule::Frequency::None)
1109 ✗ return rule;
1110 2 bool changed = false;
1111
5/6
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 20 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 30 taken 1 time.
2 if (parsed.frequency == RecurrenceRule::Frequency::Weekly && !parsed.byDay.isEmpty()) {
1112
4/4
✓ Branch 22 → 23 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 28 → 25 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
2 for (auto &day : parsed.byDay) {
1113 1 day.weekday = (day.weekday - 1 + dayDelta % 7 + 7) % 7 + 1;
1114 }
1115 1 changed = true;
1116 }
1117
5/6
✓ Branch 30 → 31 taken 1 time.
✓ Branch 30 → 34 taken 1 time.
✓ Branch 32 → 33 taken 1 time.
✗ Branch 32 → 34 not taken.
✓ Branch 35 → 36 taken 1 time.
✓ Branch 35 → 50 taken 1 time.
2 if (parsed.frequency == RecurrenceRule::Frequency::Monthly && !parsed.byMonthDay.isEmpty()) {
1118
4/4
✓ Branch 36 → 37 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 48 → 39 taken 1 time.
✓ Branch 48 → 49 taken 1 time.
2 for (int &monthDay : parsed.byMonthDay) {
1119 // S106-301: a leftward move past the 1st counts from the month's end
1120 // (day 1 one day earlier is BYMONTHDAY=-1, the last day); 0 is no
1121 // valid day and the former plain modulo produced it.
1122
1/2
✓ Branch 40 → 41 taken 1 time.
✗ Branch 40 → 46 not taken.
1 if (monthDay > 0) {
1123 1 const int moved = monthDay + dayDelta;
1124
1/2
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 1 time.
1 if (moved >= 1)
1125 ✗ monthDay = (moved - 1) % 31 + 1;
1126
1/2
✓ Branch 43 → 44 taken 1 time.
✗ Branch 43 → 45 not taken.
1 else if (moved >= -30)
1127 1 monthDay = moved - 1;
1128 else
1129 ✗ monthDay = ((moved - 1) % 31 + 31) % 31 + 1;
1130 }
1131 }
1132 1 changed = true;
1133 }
1134
1/2
✗ Branch 51 → 52 not taken.
✓ Branch 51 → 89 taken 2 times.
2 if (!parsed.until.isEmpty()) {
1135 ✗ const QDateTime until = QDateTime::fromString(parsed.until, Qt::ISODate);
1136 ✗ if (until.isValid()) {
1137 ✗ const QDateTime shifted = until.addDays(dayDelta);
1138 ✗ parsed.until = shifted.toUTC().toString(QStringLiteral("yyyyMMddTHHmmssZ"));
1139 ✗ changed = true;
1140 ✗ } else {
1141 ✗ const QDate untilDay = QDate::fromString(parsed.until, QStringLiteral("yyyyMMdd"));
1142 ✗ if (untilDay.isValid()) {
1143 ✗ parsed.until = untilDay.addDays(dayDelta).toString(QStringLiteral("yyyyMMdd"));
1144 ✗ changed = true;
1145 }
1146 }
1147 ✗ }
1148
2/3
✓ Branch 89 → 90 taken 2 times.
✗ Branch 89 → 91 not taken.
✓ Branch 90 → 92 taken 2 times.
2 return changed ? parsed.toString() : rule;
1149 2 }
1150 } // namespace
1151
1152 13 QVariantMap QuickPimBackend::moveOccurrence(const QVariantMap &occurrence, int deltaMinutes,
1153 int resizeMinutes, const QString &scope) {
1154
2/4
✓ Branch 8 → 9 taken 13 times.
✓ Branch 8 → 10 taken 13 times.
✗ Branch 331 → 332 not taken.
✗ Branch 331 → 333 not taken.
52 QVariantMap rejected{{QStringLiteral("ok"), false}};
1155 13 auto *sync = m_session.calendarSync();
1156
1/1
✓ Branch 13 → 14 taken 13 times.
13 const OccurrenceRef ref = refOf(occurrence);
1157
2/3
✓ Branch 14 → 15 taken 13 times.
✗ Branch 14 → 16 not taken.
✓ Branch 15 → 17 taken 13 times.
13 const auto master = sync ? sync->eventFor(ref.uid, ref.accountId, ref.calendarPath) : std::nullopt;
1158
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 31 taken 13 times.
13 if (!master) {
1159 ✗ rejected.insert(QStringLiteral("error"), tr("The event no longer exists."));
1160 ✗ return rejected;
1161 }
1162
5/5
✓ Branch 31 → 32 taken 13 times.
✓ Branch 32 → 33 taken 13 times.
✓ Branch 33 → 34 taken 13 times.
✓ Branch 110 → 35 taken 20 times.
✓ Branch 110 → 111 taken 13 times.
33 for (const auto &choice : calendarChoices()) {
1163
1/1
✓ Branch 36 → 37 taken 20 times.
20 const QVariantMap map = choice.toMap();
1164
21/48
✓ Branch 42 → 43 taken 20 times.
✓ Branch 43 → 44 taken 20 times.
✓ Branch 44 → 45 taken 13 times.
✓ Branch 44 → 61 taken 7 times.
✓ Branch 50 → 51 taken 13 times.
✓ Branch 51 → 52 taken 13 times.
✓ Branch 52 → 53 taken 13 times.
✗ Branch 52 → 61 not taken.
✓ Branch 72 → 73 taken 13 times.
✓ Branch 72 → 74 taken 7 times.
✓ Branch 74 → 75 taken 13 times.
✓ Branch 74 → 76 taken 7 times.
✓ Branch 76 → 77 taken 13 times.
✓ Branch 76 → 78 taken 7 times.
✓ Branch 78 → 79 taken 13 times.
✓ Branch 78 → 80 taken 7 times.
✓ Branch 80 → 81 taken 20 times.
✗ Branch 80 → 82 not taken.
✓ Branch 82 → 83 taken 20 times.
✗ Branch 82 → 84 not taken.
✓ Branch 84 → 85 taken 20 times.
✗ Branch 84 → 86 not taken.
✓ Branch 86 → 87 taken 20 times.
✗ Branch 86 → 88 not taken.
✓ Branch 88 → 89 taken 20 times.
✗ Branch 88 → 90 not taken.
✗ Branch 90 → 91 not taken.
✓ Branch 90 → 103 taken 20 times.
✗ Branch 375 → 376 not taken.
✗ Branch 375 → 377 not taken.
✗ Branch 379 → 380 not taken.
✗ Branch 379 → 381 not taken.
✗ Branch 383 → 384 not taken.
✗ Branch 383 → 385 not taken.
✗ Branch 387 → 388 not taken.
✗ Branch 387 → 389 not taken.
✗ Branch 391 → 392 not taken.
✗ Branch 391 → 393 not taken.
✗ Branch 395 → 396 not taken.
✗ Branch 395 → 397 not taken.
✗ Branch 399 → 400 not taken.
✗ Branch 399 → 401 not taken.
✗ Branch 403 → 404 not taken.
✗ Branch 403 → 405 not taken.
✗ Branch 407 → 408 not taken.
✗ Branch 407 → 409 not taken.
✗ Branch 411 → 412 not taken.
✗ Branch 411 → 413 not taken.
86 if (map.value(QStringLiteral("accountId")) == ref.accountId && map.value(QStringLiteral("path")) == ref.calendarPath &&
1165
13/22
✓ Branch 57 → 58 taken 13 times.
✓ Branch 58 → 59 taken 13 times.
✗ Branch 59 → 60 not taken.
✓ Branch 59 → 61 taken 13 times.
✓ Branch 62 → 63 taken 13 times.
✓ Branch 62 → 64 taken 7 times.
✓ Branch 64 → 65 taken 13 times.
✓ Branch 64 → 66 taken 7 times.
✓ Branch 66 → 67 taken 13 times.
✓ Branch 66 → 68 taken 7 times.
✓ Branch 68 → 69 taken 13 times.
✓ Branch 68 → 70 taken 7 times.
✓ Branch 70 → 71 taken 13 times.
✓ Branch 70 → 72 taken 7 times.
✗ Branch 359 → 360 not taken.
✗ Branch 359 → 361 not taken.
✗ Branch 363 → 364 not taken.
✗ Branch 363 → 365 not taken.
✗ Branch 367 → 368 not taken.
✗ Branch 367 → 369 not taken.
✗ Branch 371 → 372 not taken.
✗ Branch 371 → 373 not taken.
46 map.value(QStringLiteral("readOnly")).toBool()) {
1166 ✗ rejected.insert(QStringLiteral("error"), tr("This calendar is read-only."));
1167 ✗ return rejected;
1168 }
1169 33 }
1170 13 const bool recurring = !master->rrule.isEmpty();
1171 13 std::optional<CalendarEvent> exception;
1172
1/2
✗ Branch 118 → 120 not taken.
✓ Branch 118 → 127 taken 13 times.
13 if (!ref.recurrenceIso.isEmpty())
1173 ✗ exception = sync->store()->occurrenceEvent(ref.uid, ref.accountId, ref.calendarPath,
1174 ✗ QDateTime::fromString(ref.recurrenceIso, Qt::ISODate));
1175
1/2
✗ Branch 128 → 129 not taken.
✓ Branch 128 → 130 taken 13 times.
13 CalendarEvent base = exception ? *exception : *master;
1176 13 QString editScope;
1177
6/6
✓ Branch 132 → 133 taken 6 times.
✓ Branch 132 → 137 taken 7 times.
✓ Branch 135 → 136 taken 3 times.
✓ Branch 135 → 137 taken 3 times.
✓ Branch 138 → 139 taken 3 times.
✓ Branch 138 → 190 taken 10 times.
13 if (recurring && scope != QLatin1String("series")) {
1178 // CAL-DND-CREATE follow-up: "following" splits the series at this occurrence.
1179
6/6
✓ Branch 141 → 142 taken 1 time.
✓ Branch 141 → 145 taken 2 times.
✓ Branch 150 → 151 taken 2 times.
✓ Branch 150 → 152 taken 1 time.
✓ Branch 152 → 153 taken 1 time.
✓ Branch 152 → 154 taken 2 times.
6 editScope = scope == QLatin1String("following") ? QStringLiteral("following") : QStringLiteral("this");
1180
2/6
✗ Branch 155 → 156 not taken.
✓ Branch 155 → 159 taken 3 times.
✗ Branch 158 → 159 not taken.
✗ Branch 158 → 160 not taken.
✓ Branch 161 → 162 taken 3 times.
✗ Branch 161 → 198 not taken.
3 if (!exception || editScope == QLatin1String("following")) {
1181 // The occurrence's own instants with the series' duration.
1182
3/4
✓ Branch 163 → 164 taken 3 times.
✓ Branch 164 → 165 taken 3 times.
✗ Branch 164 → 169 not taken.
✓ Branch 167 → 168 taken 3 times.
3 const qint64 span = master->dtEnd.isValid() ? master->dtStart.secsTo(master->dtEnd) : 0;
1183
1/1
✓ Branch 170 → 171 taken 3 times.
3 const QDateTime start = QDateTime::fromString(ref.startIso, Qt::ISODate);
1184
3/15
✗ Branch 172 → 173 not taken.
✓ Branch 172 → 178 taken 3 times.
✗ Branch 173 → 174 not taken.
✗ Branch 174 → 175 not taken.
✗ Branch 175 → 176 not taken.
✗ Branch 176 → 177 not taken.
✗ Branch 177 → 179 not taken.
✗ Branch 181 → 182 not taken.
✓ Branch 181 → 183 taken 3 times.
✗ Branch 183 → 184 not taken.
✓ Branch 183 → 185 taken 3 times.
✗ Branch 437 → 438 not taken.
✗ Branch 437 → 439 not taken.
✗ Branch 442 → 443 not taken.
✗ Branch 442 → 444 not taken.
3 base.dtStart = master->allDay ? QDateTime(start.toUTC().date(), QTime(0, 0), QTimeZone::utc()) : start;
1185
1/1
✓ Branch 185 → 186 taken 3 times.
3 base.dtEnd = base.dtStart.addSecs(span);
1186 3 }
1187
2/2
✓ Branch 190 → 191 taken 3 times.
✓ Branch 190 → 198 taken 7 times.
10 } else if (recurring) {
1188 3 editScope = QStringLiteral("series");
1189 }
1190
2/2
✓ Branch 198 → 199 taken 1 time.
✓ Branch 198 → 218 taken 12 times.
13 if (base.allDay) {
1191
1/1
✓ Branch 199 → 200 taken 1 time.
1 base.dtStart = base.dtStart.addDays(deltaMinutes / 1440);
1192
2/4
✓ Branch 202 → 203 taken 1 time.
✓ Branch 203 → 204 taken 1 time.
✗ Branch 203 → 205 not taken.
✗ Branch 205 → 206 not taken.
1 const QDateTime end = base.dtEnd.isValid() ? base.dtEnd : base.dtStart.addDays(1);
1193
1/1
✓ Branch 206 → 207 taken 1 time.
1 base.dtEnd = end.addDays((deltaMinutes + resizeMinutes) / 1440);
1194
2/4
✓ Branch 209 → 210 taken 1 time.
✗ Branch 211 → 212 not taken.
✓ Branch 211 → 216 taken 1 time.
✗ Branch 212 → 213 not taken.
1 if (base.dtEnd <= base.dtStart) base.dtEnd = base.dtStart.addDays(1);
1195 1 } else {
1196
2/4
✓ Branch 218 → 219 taken 12 times.
✓ Branch 219 → 220 taken 12 times.
✗ Branch 219 → 221 not taken.
✗ Branch 221 → 222 not taken.
12 const QDateTime end = base.dtEnd.isValid() ? base.dtEnd : base.dtStart.addSecs(3600);
1197
1/1
✓ Branch 222 → 223 taken 12 times.
12 base.dtStart = base.dtStart.addSecs(qint64(deltaMinutes) * 60);
1198
1/1
✓ Branch 225 → 226 taken 12 times.
12 base.dtEnd = end.addSecs(qint64(deltaMinutes + resizeMinutes) * 60);
1199
4/4
✓ Branch 228 → 229 taken 12 times.
✓ Branch 229 → 230 taken 1 time.
✓ Branch 229 → 234 taken 11 times.
✓ Branch 230 → 231 taken 1 time.
12 if (base.dtStart.secsTo(base.dtEnd) < 15 * 60) base.dtEnd = base.dtStart.addSecs(15 * 60);
1200 12 }
1201
1/1
✓ Branch 237 → 238 taken 13 times.
13 QVariantMap fields = editorFields(base, {});
1202
1/1
✓ Branch 243 → 244 taken 13 times.
26 fields.insert(QStringLiteral("editScope"), editScope);
1203
1/1
✓ Branch 264 → 265 taken 13 times.
26 fields.insert(QStringLiteral("occurrenceStart"),
1204
3/4
✓ Branch 249 → 250 taken 2 times.
✓ Branch 249 → 255 taken 11 times.
✓ Branch 251 → 252 taken 2 times.
✗ Branch 251 → 253 not taken.
37 editScope == QLatin1String("this") ? (ref.recurrenceIso.isEmpty() ? ref.startIso : ref.recurrenceIso)
1205
2/2
✓ Branch 257 → 258 taken 1 time.
✓ Branch 257 → 259 taken 10 times.
11 : editScope == QLatin1String("following") ? ref.startIso : QString());
1206 // F-45: a whole-day series move shifts the rule anchors with the start.
1207
2/2
✓ Branch 271 → 272 taken 2 times.
✓ Branch 271 → 273 taken 11 times.
13 const QString storedRule = editScope == QLatin1String("this") ? QString()
1208 11 : deltaMinutes % 1440 == 0
1209
2/2
✓ Branch 273 → 274 taken 5 times.
✓ Branch 273 → 276 taken 6 times.
11 ? shiftSeriesRuleAnchors(master->rrule, deltaMinutes / 1440)
1210
1/1
✓ Branch 275 → 278 taken 5 times.
13 : master->rrule;
1211
1/1
✓ Branch 282 → 283 taken 13 times.
26 fields.insert(QStringLiteral("rrule"), storedRule);
1212
1/1
✓ Branch 289 → 290 taken 13 times.
13 fields.remove(QStringLiteral("reminders")); // moving keeps the stored reminders
1213
10/10
✓ Branch 292 → 293 taken 13 times.
✓ Branch 296 → 297 taken 13 times.
✓ Branch 297 → 298 taken 13 times.
✓ Branch 298 → 299 taken 13 times.
✓ Branch 299 → 300 taken 13 times.
✓ Branch 300 → 301 taken 13 times.
✓ Branch 301 → 302 taken 13 times.
✓ Branch 302 → 303 taken 13 times.
✓ Branch 317 → 294 taken 13 times.
✓ Branch 317 → 318 taken 13 times.
26 qCInfo(lcPimBackend) << "Move occurrence" << ref.uid << "by" << deltaMinutes << "min, resize" << resizeMinutes
1214
6/8
✓ Branch 303 → 304 taken 13 times.
✓ Branch 305 → 306 taken 7 times.
✓ Branch 305 → 309 taken 6 times.
✓ Branch 310 → 311 taken 13 times.
✓ Branch 312 → 313 taken 7 times.
✓ Branch 312 → 314 taken 6 times.
✗ Branch 507 → 508 not taken.
✗ Branch 507 → 509 not taken.
20 << "scope" << (editScope.isEmpty() ? QStringLiteral("single") : editScope);
1215
1/1
✓ Branch 318 → 319 taken 13 times.
13 return saveEvent(fields);
1216 52 }
1217
1218 2 void QuickPimBackend::showAllOccurrences(const QVariantMap &occurrence) {
1219 2 CalendarEvent series;
1220
2/2
✓ Branch 7 → 8 taken 2 times.
✓ Branch 8 → 9 taken 2 times.
4 series.accountId = occurrence.value(QStringLiteral("accountId")).toString();
1221
2/2
✓ Branch 19 → 20 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
4 series.calendarPath = occurrence.value(QStringLiteral("calendarPath")).toString();
1222
2/2
✓ Branch 31 → 32 taken 2 times.
✓ Branch 32 → 33 taken 2 times.
4 series.uid = occurrence.value(QStringLiteral("uid")).toString();
1223
5/5
✓ Branch 39 → 40 taken 2 times.
✓ Branch 43 → 44 taken 2 times.
✓ Branch 44 → 45 taken 2 times.
✓ Branch 48 → 41 taken 2 times.
✓ Branch 48 → 49 taken 2 times.
4 qCInfo(lcPimBackend) << "Showing every occurrence of a limited series";
1224
2/2
✓ Branch 49 → 50 taken 2 times.
✓ Branch 50 → 51 taken 2 times.
2 m_calendar.showAllOccurrences(seriesKey(series));
1225
1/4
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 57 taken 2 times.
✗ Branch 53 → 54 not taken.
✗ Branch 54 → 55 not taken.
2 if (m_agendaPanel) m_agendaPanel->showAllOccurrences(seriesKey(series));
1226 2 }
1227
1228 48 QVariantMap QuickPimBackend::eventDetails(const QVariantMap &occurrence) const {
1229 48 const auto *sync = m_session.calendarSync();
1230
1/1
✓ Branch 3 → 4 taken 48 times.
48 const OccurrenceRef ref = refOf(occurrence);
1231
3/6
✓ Branch 4 → 5 taken 48 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 48 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 48 times.
48 if (!sync || !sync->store())
1232 ✗ return {};
1233
1/1
✓ Branch 11 → 12 taken 48 times.
48 const auto master = sync->eventFor(ref.uid, ref.accountId, ref.calendarPath);
1234 48 std::optional<CalendarEvent> exception;
1235
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 21 taken 47 times.
48 if (!ref.recurrenceIso.isEmpty())
1236
1/1
✓ Branch 16 → 17 taken 1 time.
3 exception = sync->store()->occurrenceEvent(ref.uid, ref.accountId, ref.calendarPath,
1237
1/1
✓ Branch 15 → 16 taken 1 time.
3 QDateTime::fromString(ref.recurrenceIso, Qt::ISODate));
1238
3/3
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 24 taken 47 times.
✓ Branch 25 → 26 taken 48 times.
48 const std::optional<CalendarEvent> event = exception ? exception : master;
1239
1/2
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 48 times.
48 if (!event)
1240 ✗ return {};
1241 // The occurrence's own instants (a series master only knows the first).
1242
2/2
✓ Branch 29 → 30 taken 48 times.
✓ Branch 30 → 31 taken 48 times.
96 const QDateTime start = QDateTime::fromString(ref.startIso, Qt::ISODate).isValid()
1243
2/2
✓ Branch 31 → 32 taken 47 times.
✓ Branch 31 → 33 taken 1 time.
48 ? QDateTime::fromString(ref.startIso, Qt::ISODate)
1244
1/1
✓ Branch 32 → 35 taken 47 times.
48 : event->dtStart;
1245
3/4
✓ Branch 37 → 38 taken 48 times.
✓ Branch 38 → 39 taken 48 times.
✗ Branch 38 → 43 not taken.
✓ Branch 41 → 42 taken 48 times.
48 const qint64 seconds = event->dtEnd.isValid() ? event->dtStart.secsTo(event->dtEnd) : 0;
1246
1/1
✓ Branch 44 → 45 taken 48 times.
48 const QDateTime end = start.addSecs(seconds);
1247
1/1
✓ Branch 45 → 46 taken 48 times.
48 const QLocale locale;
1248 48 QString dateText, timeText;
1249
1/2
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 82 taken 48 times.
48 if (event->allDay) {
1250 ✗ const QDate first = start.toUTC().date(), last = qMax(first, end.toUTC().date().addDays(-1));
1251 ✗ dateText = first == last ? locale.toString(first, QLocale::LongFormat)
1252 ✗ : locale.toString(first, QLocale::LongFormat) + QStringLiteral(" – ") +
1253 ✗ locale.toString(last, QLocale::LongFormat);
1254 ✗ timeText = tr("All day");
1255 } else {
1256
2/2
✓ Branch 82 → 83 taken 48 times.
✓ Branch 83 → 84 taken 48 times.
48 const QDateTime localStart = start.toLocalTime(), localEnd = end.toLocalTime();
1257
2/2
✓ Branch 84 → 85 taken 48 times.
✓ Branch 85 → 86 taken 48 times.
48 dateText = locale.toString(localStart.date(), QLocale::LongFormat);
1258
2/2
✓ Branch 88 → 89 taken 48 times.
✓ Branch 89 → 90 taken 48 times.
48 timeText = locale.toString(localStart.time(), QLocale::ShortFormat);
1259
1/2
✓ Branch 92 → 93 taken 48 times.
✗ Branch 92 → 110 not taken.
48 if (seconds > 0)
1260 96 timeText += QStringLiteral(" – ") +
1261
7/7
✓ Branch 93 → 94 taken 48 times.
✓ Branch 94 → 95 taken 48 times.
✓ Branch 96 → 97 taken 1 time.
✓ Branch 96 → 98 taken 47 times.
✓ Branch 97 → 100 taken 1 time.
✓ Branch 98 → 99 taken 47 times.
✓ Branch 99 → 100 taken 47 times.
96 (localEnd.date() != localStart.date() ? locale.toString(localEnd, QLocale::ShortFormat)
1262
2/2
✓ Branch 103 → 104 taken 48 times.
✓ Branch 104 → 105 taken 48 times.
48 : locale.toString(localEnd.time(), QLocale::ShortFormat));
1263 48 }
1264 // A zone other than the viewer's is named with its own wall clock.
1265 48 QString zoneText;
1266
4/5
✓ Branch 114 → 115 taken 48 times.
✗ Branch 114 → 125 not taken.
✓ Branch 116 → 117 taken 48 times.
✓ Branch 117 → 118 taken 22 times.
✓ Branch 117 → 125 taken 26 times.
70 if (!event->allDay && event->dtStart.timeSpec() == Qt::TimeZone &&
1267
12/16
✓ Branch 119 → 120 taken 22 times.
✓ Branch 120 → 121 taken 22 times.
✓ Branch 121 → 122 taken 22 times.
✓ Branch 122 → 123 taken 22 times.
✓ Branch 123 → 124 taken 13 times.
✓ Branch 123 → 125 taken 9 times.
✓ Branch 126 → 127 taken 22 times.
✓ Branch 126 → 128 taken 26 times.
✓ Branch 128 → 129 taken 22 times.
✓ Branch 128 → 130 taken 26 times.
✓ Branch 130 → 131 taken 13 times.
✓ Branch 130 → 152 taken 35 times.
✗ Branch 657 → 658 not taken.
✗ Branch 657 → 659 not taken.
✗ Branch 661 → 662 not taken.
✗ Branch 661 → 663 not taken.
70 event->dtStart.timeZone().offsetFromUtc(start) != QTimeZone::systemTimeZone().offsetFromUtc(start)) {
1268
2/2
✓ Branch 132 → 133 taken 13 times.
✓ Branch 133 → 134 taken 13 times.
13 const QDateTime there = start.toTimeZone(event->dtStart.timeZone());
1269
4/4
✓ Branch 135 → 136 taken 13 times.
✓ Branch 140 → 141 taken 13 times.
✓ Branch 141 → 142 taken 13 times.
✓ Branch 142 → 143 taken 13 times.
39 zoneText = tr("%1 in %2").arg(locale.toString(there.time(), QLocale::ShortFormat),
1270
3/3
✓ Branch 137 → 138 taken 13 times.
✓ Branch 138 → 139 taken 13 times.
✓ Branch 139 → 140 taken 13 times.
39 QString::fromUtf8(event->dtStart.timeZone().id()));
1271 13 }
1272
2/5
✓ Branch 155 → 156 taken 48 times.
✓ Branch 159 → 160 taken 48 times.
✗ Branch 159 → 161 not taken.
✗ Branch 687 → 688 not taken.
✗ Branch 687 → 689 not taken.
144 const QVariantMap recurrence = master ? describeRecurrence(master->rrule, master->dtStart.toString(Qt::ISODate))
1273
2/3
✓ Branch 153 → 154 taken 48 times.
✗ Branch 153 → 158 not taken.
✓ Branch 157 → 159 taken 48 times.
96 : QVariantMap{};
1274
1/1
✓ Branch 161 → 162 taken 48 times.
48 const auto pending = sync->pendingWrite(ref.uid, ref.accountId, ref.calendarPath);
1275 48 QString calendarName = ref.calendarPath, color;
1276 48 bool readOnly = true;
1277
5/5
✓ Branch 163 → 164 taken 48 times.
✓ Branch 164 → 165 taken 48 times.
✓ Branch 165 → 166 taken 48 times.
✓ Branch 246 → 167 taken 115 times.
✓ Branch 246 → 247 taken 48 times.
163 for (const auto &choice : calendarChoices()) {
1278
1/1
✓ Branch 168 → 169 taken 115 times.
115 const QVariantMap map = choice.toMap();
1279
25/50
✓ Branch 174 → 175 taken 115 times.
✓ Branch 175 → 176 taken 115 times.
✓ Branch 176 → 177 taken 109 times.
✓ Branch 176 → 186 taken 6 times.
✓ Branch 182 → 183 taken 109 times.
✓ Branch 183 → 184 taken 109 times.
✓ Branch 184 → 185 taken 48 times.
✓ Branch 184 → 186 taken 61 times.
✓ Branch 187 → 188 taken 109 times.
✓ Branch 187 → 189 taken 6 times.
✓ Branch 189 → 190 taken 109 times.
✓ Branch 189 → 191 taken 6 times.
✓ Branch 191 → 192 taken 109 times.
✓ Branch 191 → 193 taken 6 times.
✓ Branch 193 → 194 taken 109 times.
✓ Branch 193 → 195 taken 6 times.
✓ Branch 195 → 196 taken 109 times.
✓ Branch 195 → 197 taken 6 times.
✓ Branch 197 → 198 taken 115 times.
✗ Branch 197 → 199 not taken.
✓ Branch 199 → 200 taken 115 times.
✗ Branch 199 → 201 not taken.
✓ Branch 201 → 202 taken 115 times.
✗ Branch 201 → 203 not taken.
✓ Branch 203 → 204 taken 115 times.
✗ Branch 203 → 205 not taken.
✓ Branch 205 → 206 taken 115 times.
✗ Branch 205 → 207 not taken.
✓ Branch 207 → 208 taken 48 times.
✓ Branch 207 → 243 taken 67 times.
✗ Branch 691 → 692 not taken.
✗ Branch 691 → 693 not taken.
✗ Branch 695 → 696 not taken.
✗ Branch 695 → 697 not taken.
✗ Branch 699 → 700 not taken.
✗ Branch 699 → 701 not taken.
✗ Branch 703 → 704 not taken.
✗ Branch 703 → 705 not taken.
✗ Branch 707 → 708 not taken.
✗ Branch 707 → 709 not taken.
✗ Branch 711 → 712 not taken.
✗ Branch 711 → 713 not taken.
✗ Branch 715 → 716 not taken.
✗ Branch 715 → 717 not taken.
✗ Branch 719 → 720 not taken.
✗ Branch 719 → 721 not taken.
✗ Branch 723 → 724 not taken.
✗ Branch 723 → 725 not taken.
✗ Branch 727 → 728 not taken.
✗ Branch 727 → 729 not taken.
339 if (map.value(QStringLiteral("accountId")) == ref.accountId && map.value(QStringLiteral("path")) == ref.calendarPath) {
1280
2/2
✓ Branch 212 → 213 taken 48 times.
✓ Branch 213 → 214 taken 48 times.
96 calendarName = map.value(QStringLiteral("displayName")).toString();
1281
2/2
✓ Branch 224 → 225 taken 48 times.
✓ Branch 225 → 226 taken 48 times.
96 color = map.value(QStringLiteral("color")).toString();
1282
2/2
✓ Branch 236 → 237 taken 48 times.
✓ Branch 237 → 238 taken 48 times.
96 readOnly = map.value(QStringLiteral("readOnly")).toBool();
1283 }
1284 163 }
1285 48 const qint64 minutes = seconds / 60;
1286 // Sprint 103 S103.6.1: the people the appointment is with (max. 3 + "+N").
1287 48 QStringList with;
1288 48 QVariantList people;
1289
6/6
✓ Branch 252 → 253 taken 48 times.
✓ Branch 253 → 254 taken 48 times.
✓ Branch 257 → 258 taken 48 times.
✓ Branch 258 → 259 taken 48 times.
✓ Branch 284 → 260 taken 2 times.
✓ Branch 284 → 285 taken 48 times.
50 for (const EventPerson &person : ICalEventCodec::peopleOf(ICal::parseFirst(event->icalRaw, QStringLiteral("VEVENT")))) {
1290
1/1
✓ Branch 261 → 262 taken 2 times.
2 with.append(person.name);
1291
3/5
✓ Branch 272 → 273 taken 2 times.
✓ Branch 276 → 277 taken 4 times.
✓ Branch 276 → 278 taken 2 times.
✗ Branch 789 → 790 not taken.
✗ Branch 789 → 791 not taken.
10 people.append(QVariantMap{{QStringLiteral("uid"), person.uid}, {QStringLiteral("name"), person.name}});
1292 48 }
1293
2/2
✓ Branch 294 → 295 taken 48 times.
✓ Branch 298 → 299 taken 48 times.
144 const QString withText = with.mid(0, 3).join(QStringLiteral(", ")) +
1294
3/9
✗ Branch 287 → 288 not taken.
✓ Branch 287 → 293 taken 48 times.
✗ Branch 289 → 290 not taken.
✗ Branch 292 → 294 not taken.
✓ Branch 299 → 300 taken 48 times.
✗ Branch 305 → 306 not taken.
✓ Branch 305 → 307 taken 48 times.
✗ Branch 831 → 832 not taken.
✗ Branch 831 → 833 not taken.
144 (with.size() > 3 ? QLatin1Char(' ') + tr("+%n", "", int(with.size()) - 3) : QString());
1295 96 return {{QStringLiteral("summary"), event->summary},
1296 96 {QStringLiteral("withText"), withText},
1297 96 {QStringLiteral("people"), people},
1298
1/1
✓ Branch 324 → 325 taken 48 times.
96 {QStringLiteral("reminderText"), CalendarEventDetails::reminderText(event->reminders)},
1299 96 {QStringLiteral("calendarName"), calendarName},
1300 96 {QStringLiteral("color"), color},
1301 96 {QStringLiteral("readOnly"), readOnly},
1302 96 {QStringLiteral("dateText"), dateText},
1303 96 {QStringLiteral("timeText"), timeText},
1304
2/3
✗ Branch 350 → 351 not taken.
✓ Branch 350 → 352 taken 48 times.
✓ Branch 352 → 353 taken 48 times.
96 {QStringLiteral("durationText"), event->allDay ? QString() : CalendarEventDetails::durationText(minutes)},
1305 96 {QStringLiteral("zoneText"), zoneText},
1306
4/5
✓ Branch 362 → 363 taken 48 times.
✗ Branch 362 → 367 not taken.
✓ Branch 364 → 365 taken 48 times.
✓ Branch 365 → 366 taken 14 times.
✓ Branch 365 → 367 taken 34 times.
96 {QStringLiteral("recurring"), master && CalendarSeries::isRecurring(*master)},
1307 96 {QStringLiteral("isException"), exception.has_value()},
1308
3/3
✓ Branch 380 → 381 taken 48 times.
✓ Branch 381 → 382 taken 14 times.
✓ Branch 381 → 389 taken 34 times.
144 {QStringLiteral("recurrenceText"), master && CalendarSeries::isRecurring(*master)
1309
11/20
✓ Branch 378 → 379 taken 48 times.
✗ Branch 378 → 389 not taken.
✓ Branch 386 → 387 taken 14 times.
✓ Branch 387 → 388 taken 14 times.
✓ Branch 541 → 542 taken 14 times.
✓ Branch 541 → 543 taken 34 times.
✓ Branch 543 → 544 taken 14 times.
✓ Branch 543 → 545 taken 34 times.
✓ Branch 545 → 546 taken 14 times.
✓ Branch 545 → 547 taken 34 times.
✓ Branch 547 → 548 taken 14 times.
✓ Branch 547 → 549 taken 34 times.
✗ Branch 959 → 960 not taken.
✗ Branch 959 → 961 not taken.
✗ Branch 963 → 964 not taken.
✗ Branch 963 → 965 not taken.
✗ Branch 967 → 968 not taken.
✗ Branch 967 → 969 not taken.
✗ Branch 971 → 972 not taken.
✗ Branch 971 → 973 not taken.
158 ? recurrence.value(QStringLiteral("text")).toString() : QString()},
1310
2/2
✓ Branch 398 → 399 taken 48 times.
✓ Branch 399 → 400 taken 48 times.
144 {QStringLiteral("partial"), occurrence.value(QStringLiteral("partial")).toBool()},
1311
2/2
✓ Branch 408 → 409 taken 48 times.
✓ Branch 409 → 410 taken 48 times.
144 {QStringLiteral("limited"), occurrence.value(QStringLiteral("limited")).toBool()},
1312 96 {QStringLiteral("location"), event->location},
1313 96 {QStringLiteral("description"), event->description},
1314
1/1
✓ Branch 425 → 426 taken 48 times.
96 {QStringLiteral("descriptionHtml"), CalendarEventDetails::linkified(event->description)},
1315
1/1
✓ Branch 431 → 432 taken 48 times.
96 {QStringLiteral("meetingUrl"), CalendarEventDetails::meetingUrl(*event)},
1316
4/4
✓ Branch 437 → 438 taken 9 times.
✓ Branch 437 → 443 taken 39 times.
✓ Branch 441 → 442 taken 8 times.
✓ Branch 441 → 443 taken 1 time.
96 {QStringLiteral("pending"), pending && pending->state == QLatin1String("pending")},
1317
4/4
✓ Branch 449 → 450 taken 9 times.
✓ Branch 449 → 455 taken 39 times.
✓ Branch 453 → 454 taken 1 time.
✓ Branch 453 → 455 taken 8 times.
96 {QStringLiteral("conflict"), pending && pending->state == QLatin1String("conflict")},
1318
5/6
✓ Branch 461 → 462 taken 9 times.
✓ Branch 461 → 470 taken 39 times.
✓ Branch 465 → 466 taken 1 time.
✓ Branch 465 → 470 taken 8 times.
✗ Branch 468 → 469 not taken.
✓ Branch 468 → 470 taken 1 time.
97 {QStringLiteral("deletedOnServer"), pending && pending->state == QLatin1String("conflict") &&
1319 1 pending->serverIcal.isEmpty()},
1320
3/4
✓ Branch 476 → 477 taken 9 times.
✓ Branch 476 → 480 taken 39 times.
✗ Branch 478 → 479 not taken.
✓ Branch 478 → 480 taken 9 times.
96 {QStringLiteral("retrying"), pending && pending->attempts > 0},
1321
2/2
✓ Branch 486 → 487 taken 9 times.
✓ Branch 486 → 489 taken 39 times.
96 {QStringLiteral("lastError"), pending ? pending->lastError : QString()},
1322
4/6
✓ Branch 495 → 496 taken 9 times.
✓ Branch 495 → 498 taken 39 times.
✓ Branch 502 → 503 taken 1248 times.
✓ Branch 502 → 504 taken 48 times.
✗ Branch 837 → 838 not taken.
✗ Branch 837 → 839 not taken.
1344 {QStringLiteral("conflictFields"), pending ? pending->conflictFields : QStringList()}};
1323 1350 }
1324
1325 1 QVariantMap QuickPimBackend::duplicateEvent(const QVariantMap &occurrence) {
1326
2/4
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 1 time.
✗ Branch 218 → 219 not taken.
✗ Branch 218 → 220 not taken.
4 QVariantMap rejected{{QStringLiteral("ok"), false}};
1327 1 auto *sync = m_session.calendarSync();
1328
1/1
✓ Branch 13 → 14 taken 1 time.
1 const OccurrenceRef ref = refOf(occurrence);
1329
2/3
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 16 not taken.
✓ Branch 15 → 17 taken 1 time.
1 const auto master = sync ? sync->eventFor(ref.uid, ref.accountId, ref.calendarPath) : std::nullopt;
1330
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 31 taken 1 time.
1 if (!master) {
1331 ✗ rejected.insert(QStringLiteral("error"), tr("The event no longer exists."));
1332 ✗ return rejected;
1333 }
1334 1 CalendarEvent copy = *master;
1335
1/2
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 45 taken 1 time.
1 if (!ref.recurrenceIso.isEmpty())
1336 ✗ if (const auto exception = sync->store()->occurrenceEvent(ref.uid, ref.accountId, ref.calendarPath,
1337 ✗ QDateTime::fromString(ref.recurrenceIso, Qt::ISODate)))
1338 ✗ copy = *exception;
1339
1/1
✓ Branch 45 → 46 taken 1 time.
1 const QDateTime start = QDateTime::fromString(ref.startIso, Qt::ISODate);
1340
3/4
✓ Branch 46 → 47 taken 1 time.
✓ Branch 47 → 48 taken 1 time.
✗ Branch 47 → 50 not taken.
✓ Branch 48 → 49 taken 1 time.
1 const qint64 seconds = copy.dtEnd.isValid() ? copy.dtStart.secsTo(copy.dtEnd) : -1;
1341
2/3
✓ Branch 51 → 52 taken 1 time.
✓ Branch 52 → 53 taken 1 time.
✗ Branch 52 → 73 not taken.
1 if (start.isValid())
1342
3/14
✗ Branch 53 → 54 not taken.
✓ Branch 53 → 59 taken 1 time.
✗ Branch 54 → 55 not taken.
✗ Branch 55 → 56 not taken.
✗ Branch 56 → 57 not taken.
✗ Branch 57 → 58 not taken.
✗ Branch 58 → 64 not taken.
✓ Branch 63 → 64 taken 1 time.
✗ Branch 70 → 71 not taken.
✓ Branch 70 → 72 taken 1 time.
✗ Branch 254 → 255 not taken.
✗ Branch 254 → 256 not taken.
✗ Branch 259 → 260 not taken.
✗ Branch 259 → 261 not taken.
4 copy.dtStart = copy.allDay ? QDateTime(start.toUTC().date(), QTime(0, 0), QTimeZone::utc())
1343
5/11
✓ Branch 59 → 60 taken 1 time.
✓ Branch 60 → 61 taken 1 time.
✗ Branch 60 → 62 not taken.
✓ Branch 61 → 63 taken 1 time.
✗ Branch 62 → 63 not taken.
✓ Branch 66 → 67 taken 1 time.
✗ Branch 66 → 68 not taken.
✗ Branch 68 → 69 not taken.
✓ Branch 68 → 70 taken 1 time.
✗ Branch 250 → 251 not taken.
✗ Branch 250 → 252 not taken.
2 : start.toTimeZone(copy.dtStart.timeSpec() == Qt::TimeZone ? copy.dtStart.timeZone()
1344 1 : QTimeZone::systemTimeZone());
1345
2/3
✓ Branch 73 → 74 taken 1 time.
✗ Branch 73 → 75 not taken.
✓ Branch 74 → 76 taken 1 time.
1 copy.dtEnd = seconds >= 0 ? copy.dtStart.addSecs(seconds) : QDateTime();
1346 // One appointment: no recurrence, a new resource.
1347
2/2
✓ Branch 78 → 79 taken 1 time.
✓ Branch 79 → 80 taken 1 time.
1 copy.uid = QUuid::createUuid().toString(QUuid::WithoutBraces);
1348 1 copy.id = 0;
1349 1 copy.rrule.clear();
1350
1/1
✓ Branch 83 → 84 taken 1 time.
1 copy.exdates.clear();
1351
1/1
✓ Branch 84 → 85 taken 1 time.
1 copy.rdates.clear();
1352 1 copy.recurrenceId = {};
1353 1 copy.etag.clear();
1354 1 copy.resourceHref.clear();
1355 1 copy.resourceIcal.clear();
1356 // S106-302: the copy keeps the stored VEVENT — attendees, organizer,
1357 // named people, the mail link and every unmodelled property — and the
1358 // codec patches it (a new UID, no recurrence). Only what makes it an
1359 // occurrence of the old series or a later revision goes.
1360
1/1
✓ Branch 94 → 95 taken 1 time.
1 ICal::Component vevent = ICal::parseFirst(copy.icalRaw, QStringLiteral("VEVENT"));
1361
1/2
✗ Branch 98 → 99 not taken.
✓ Branch 98 → 100 taken 1 time.
1 if (vevent.name.isEmpty()) {
1362 ✗ copy.icalRaw.clear();
1363 } else {
1364 6 for (const QString &name : {QStringLiteral("RECURRENCE-ID"), QStringLiteral("RRULE"), QStringLiteral("RDATE"),
1365
2/2
✓ Branch 121 → 119 taken 4 times.
✓ Branch 121 → 122 taken 1 time.
15 QStringLiteral("EXDATE")})
1366
3/5
✓ Branch 119 → 120 taken 4 times.
✓ Branch 123 → 124 taken 4 times.
✓ Branch 123 → 127 taken 1 time.
✗ Branch 274 → 275 not taken.
✗ Branch 274 → 278 not taken.
9 vevent.removeProperties(name);
1367
2/2
✓ Branch 128 → 129 taken 1 time.
✓ Branch 132 → 133 taken 1 time.
2 vevent.setPropertyValue(QStringLiteral("UID"), ICal::escapeText(copy.uid));
1368
1/1
✓ Branch 142 → 143 taken 1 time.
2 vevent.setPropertyValue(QStringLiteral("SEQUENCE"), QStringLiteral("0"));
1369
1/1
✓ Branch 147 → 148 taken 1 time.
1 copy.icalRaw = ICalEventCodec::componentText(vevent);
1370 }
1371
2/2
✓ Branch 151 → 152 taken 1 time.
✓ Branch 153 → 154 taken 1 time.
2 copy.summary = tr("Copy of %1").arg(copy.summary);
1372
2/3
✓ Branch 157 → 158 taken 1 time.
✗ Branch 158 → 159 not taken.
✓ Branch 158 → 171 taken 1 time.
1 if (!sync->saveEvent(copy, true)) {
1373 ✗ rejected.insert(QStringLiteral("error"), tr("The local calendar write failed."));
1374 ✗ return rejected;
1375 }
1376
4/6
✓ Branch 175 → 176 taken 1 time.
✓ Branch 176 → 177 taken 1 time.
✓ Branch 180 → 181 taken 1 time.
✓ Branch 180 → 182 taken 1 time.
✗ Branch 326 → 327 not taken.
✗ Branch 326 → 328 not taken.
2 pushUndo(tr("Appointment duplicated"), {ResourceSnapshot{copy.uid, copy.accountId, copy.calendarPath, {}}});
1377 4 return {{QStringLiteral("ok"), true}, {QStringLiteral("uid"), copy.uid},
1378
4/6
✓ Branch 193 → 194 taken 1 time.
✓ Branch 194 → 195 taken 1 time.
✓ Branch 198 → 199 taken 3 times.
✓ Branch 198 → 200 taken 1 time.
✗ Branch 346 → 347 not taken.
✗ Branch 346 → 348 not taken.
6 {QStringLiteral("startIso"), copy.dtStart.toUTC().toString(Qt::ISODate)}};
1379 10 }
1380
1381 ✗ QVariantMap QuickPimBackend::moveEventToCalendar(const QVariantMap &occurrence, const QString &accountId,
1382 const QString &calendarPath) {
1383 ✗ QVariantMap rejected{{QStringLiteral("ok"), false}};
1384 ✗ auto *sync = m_session.calendarSync();
1385 ✗ const OccurrenceRef ref = refOf(occurrence);
1386 ✗ if (!sync || !sync->store())
1387 ✗ return rejected;
1388 ✗ const auto writable = sync->calendars();
1389 ✗ const auto isWritable = [&](const QString &account, const QString &path) {
1390 ✗ return std::any_of(writable.cbegin(), writable.cend(), [&](const CalendarInfo &calendar) {
1391 ✗ return calendar.accountId == account && calendar.path == path;
1392 ✗ });
1393 ✗ };
1394 ✗ if (!isWritable(ref.accountId, ref.calendarPath) || !isWritable(accountId, calendarPath)) {
1395 ✗ rejected.insert(QStringLiteral("error"), tr("Choose an available writable calendar."));
1396 ✗ return rejected;
1397 }
1398 ✗ if (accountId == ref.accountId && calendarPath == ref.calendarPath)
1399 ✗ return {{QStringLiteral("ok"), true}};
1400 ✗ const ResourceSnapshot source = snapshot(ref.uid, ref.accountId, ref.calendarPath);
1401 ✗ if (source.rows.isEmpty()) {
1402 ✗ rejected.insert(QStringLiteral("error"), tr("The event no longer exists."));
1403 ✗ return rejected;
1404 }
1405 // The whole resource moves: create it in the target, then delete it in
1406 // the source (both through the outbox).
1407 ✗ for (CalendarEvent row : source.rows) {
1408 ✗ row.accountId = accountId;
1409 ✗ row.calendarPath = calendarPath;
1410 ✗ row.etag.clear();
1411 ✗ row.resourceHref.clear();
1412 ✗ row.resourceIcal.clear();
1413 ✗ if (!sync->saveEvent(row, true)) {
1414 // S106-303: rows already created in the target go again.
1415 ✗ sync->restoreEventSeries(ref.uid, accountId, calendarPath, {});
1416 ✗ rejected.insert(QStringLiteral("error"), tr("The local calendar write failed."));
1417 ✗ return rejected;
1418 }
1419 ✗ }
1420 ✗ if (!sync->removeEvent(ref.uid, ref.accountId, ref.calendarPath)) {
1421 // The source stays: the copy in the target goes, nothing is moved twice.
1422 ✗ sync->restoreEventSeries(ref.uid, accountId, calendarPath, {});
1423 ✗ qCWarning(lcPimBackend) << "Move to another calendar failed: the source could not be removed";
1424 ✗ rejected.insert(QStringLiteral("error"), tr("The local calendar write failed."));
1425 ✗ return rejected;
1426 }
1427 ✗ pushUndo(tr("Appointment moved to another calendar"),
1428 {source, ResourceSnapshot{ref.uid, accountId, calendarPath, {}}});
1429 ✗ return {{QStringLiteral("ok"), true}};
1430 ✗ }
1431
1432 ✗ bool QuickPimBackend::resolveEventConflict(const QVariantMap &occurrence, const QString &choice) {
1433 ✗ auto *sync = m_session.calendarSync();
1434 ✗ const OccurrenceRef ref = refOf(occurrence);
1435 ✗ return sync && sync->resolveEventConflict(ref.uid, ref.accountId, ref.calendarPath, choice);
1436 ✗ }
1437
1438 ✗ void QuickPimBackend::retryCalendarSync() {
1439 ✗ if (auto *sync = m_session.calendarSync())
1440 ✗ sync->retryPendingWrites();
1441 ✗ }
1442
1443 ✗ QVariantMap QuickPimBackend::renameEvent(const QVariantMap &occurrence, const QString &summary) {
1444 ✗ QVariantMap rejected{{QStringLiteral("ok"), false}};
1445 ✗ auto *sync = m_session.calendarSync();
1446 ✗ const OccurrenceRef ref = refOf(occurrence);
1447 ✗ const QString title = summary.trimmed();
1448 ✗ if (!sync || title.isEmpty()) {
1449 ✗ rejected.insert(QStringLiteral("error"), tr("Enter a subject."));
1450 ✗ return rejected;
1451 }
1452 ✗ const QDateTime anchor = QDateTime::fromString(ref.recurrenceIso, Qt::ISODate);
1453 ✗ auto event = anchor.isValid() ? sync->store()->occurrenceEvent(ref.uid, ref.accountId, ref.calendarPath, anchor)
1454 ✗ : sync->eventFor(ref.uid, ref.accountId, ref.calendarPath);
1455 ✗ if (!event) {
1456 ✗ rejected.insert(QStringLiteral("error"), tr("The event no longer exists."));
1457 ✗ return rejected;
1458 }
1459 ✗ if (event->summary == title)
1460 ✗ return {{QStringLiteral("ok"), true}};
1461 ✗ const ResourceSnapshot before = snapshot(ref.uid, ref.accountId, ref.calendarPath);
1462 ✗ event->summary = title;
1463 ✗ if (!sync->saveEvent(*event, false)) {
1464 ✗ rejected.insert(QStringLiteral("error"), tr("The local calendar write failed."));
1465 ✗ return rejected;
1466 }
1467 ✗ pushUndo(tr("Appointment renamed"), {before});
1468 ✗ return {{QStringLiteral("ok"), true}};
1469 ✗ }
1470
1471 1 bool QuickPimBackend::copyEventAsText(const QVariantMap &occurrence) const {
1472
1/1
✓ Branch 2 → 3 taken 1 time.
1 const QVariantMap details = eventDetails(occurrence);
1473
2/3
✓ Branch 3 → 4 taken 1 time.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1 time.
1 if (details.isEmpty())
1474 ✗ return false;
1475 1 CalendarEvent event;
1476
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
2 event.summary = details.value(QStringLiteral("summary")).toString();
1477
2/2
✓ Branch 23 → 24 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
2 event.location = details.value(QStringLiteral("location")).toString();
1478
2/2
✓ Branch 35 → 36 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
2 event.description = details.value(QStringLiteral("description")).toString();
1479
3/3
✓ Branch 56 → 57 taken 1 time.
✓ Branch 57 → 58 taken 1 time.
✓ Branch 58 → 59 taken 1 time.
3 const QString when = details.value(QStringLiteral("dateText")).toString() + QStringLiteral(", ") +
1480
3/3
✓ Branch 47 → 48 taken 1 time.
✓ Branch 48 → 49 taken 1 time.
✓ Branch 59 → 60 taken 1 time.
4 details.value(QStringLiteral("timeText")).toString();
1481
2/3
✓ Branch 73 → 74 taken 1 time.
✓ Branch 74 → 75 taken 1 time.
✗ Branch 74 → 90 not taken.
1 if (auto *clipboard = QGuiApplication::clipboard())
1482
2/2
✓ Branch 81 → 82 taken 1 time.
✓ Branch 82 → 83 taken 1 time.
1 clipboard->setText(CalendarEventDetails::plainText(event, when,
1483
2/2
✓ Branch 79 → 80 taken 1 time.
✓ Branch 80 → 81 taken 1 time.
3 details.value(QStringLiteral("recurrenceText")).toString()));
1484
2/2
✓ Branch 90 → 91 taken 1 time.
✓ Branch 91 → 92 taken 1 time.
1 m_deps.setStatus(tr("Appointment copied as text"));
1485 1 return true;
1486 1 }
1487